React (76)
Children categories
Hide Empty Word Regions and Identify Merge Fields with JavaScript in React
2026-10-16 08:16:25 Written by Amy ZhaoMail merge templates usually carry a few lines that only make sense when there is data behind them: a supplementary note, a remark, an optional cc. When the data is missing and only blank space is left behind, the layout is stretched apart, and the reader can hardly tell “this item was not included this time” from “something was left out”. The ideal outcome is for that line, together with the space it takes up, to disappear.
A related requirement turns up when a template has to be checked. When someone hands you a template, you need to know which merge fields it contains and how those fields are divided into merge regions. Expanding the field codes one by one to take stock of them is not realistic.
Spire.Doc for JavaScript handles both tasks in the browser: empty paragraphs and empty regions are cleaned up by the mail merge process itself, while region names and merge field names are read directly from MailMerge. The component runs on WebAssembly and reads and writes the template through a virtual file system (VFS), so no backend service is required.
This article covers two core features:
- Hide empty paragraphs and merge regions automatically after the merge
- Read the region names and merge field names from a Word template
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.
Hide Empty Paragraphs and Merge Regions After a Mail Merge
Prepare the Template
Two boolean properties on MailMerge control the automatic hiding of empty paragraphs and merge regions after a mail merge:
| Property | What it does | Default |
|---|---|---|
MailMerge.HideEmptyParagraphs |
Deletes paragraphs whose content is empty after the merge | false |
MailMerge.HideEmptyGroup |
Deletes merge regions whose internal paragraphs have all become empty | false |
Both properties are off by default and have to be set explicitly before MailMerge.Execute().
The sample template is a “Service Follow-up Confirmation” in which three optional constructs are written in three different ways:
| How it is written | Where it sits in the template | Merge result with hiding off |
|---|---|---|
«Optional Note» in a paragraph of its own |
after the introductory copy | kept as one blank line |
the merge region OptionalBlock (two region-marker paragraphs + two field paragraphs) |
after the delivery data row | kept as four blank lines |
Note: «Optional Remark» (label and field in one paragraph) |
before the signature | kept as the label Note: |
The part of the template that concerns hiding looks like this:
«Optional Note»
«GroupStart:OptionalBlock»
«Optional Line 1»
«Optional Line 2»
«GroupEnd:OptionalBlock»
Note: «Optional Remark»
In Word a region marker is a MERGEFIELD field like any other; only the field name is written as GroupStart:region name and GroupEnd:region name, and without quotation marks.
The Code
The steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the sample template into the virtual file system (VFS), create a
Documentobject and load the file. - Before
MailMerge.Execute(), setHideEmptyParagraphsandHideEmptyGrouptotrue. - Call
MailMerge.Execute()with the field names and their values to run the mail merge. - Save the result with
Document.SaveToFile(), read it back from the VFS and trigger the browser download.
The code for hiding empty paragraphs and merge regions after a mail merge is as follows:
function App() {
const HideEmptyRegions = 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 the sample template into the virtual file system (VFS)
let inputFileName = "HideEmptyRegions.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a document and load the template
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Provide values for three fields only; the optional fields in the
// template are left empty
let fieldNames = ["Contact Name", "Fax", "Date"];
let fieldValues = ["John Smith", "+1 (69) 123456", docModule.DateTime.get_Now().Date.ToString()];
// Hide the paragraphs and merge regions that come out empty
doc.MailMerge.HideEmptyParagraphs = true;
doc.MailMerge.HideEmptyGroup = true;
// Execute the mail merge
doc.MailMerge.Execute(fieldNames, fieldValues);
// Define the output file name and save the document
const outputFileName = "HideEmptyRegions-result.docx";
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose()
// Read the saved file and convert it to a Blob
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>Hide empty paragraphs and merge regions</h1>
<button onClick={HideEmptyRegions}>Generate</button>
</div>
);
}
export default App;
The key statements are the two lines before Execute():
doc.MailMerge.HideEmptyParagraphs = true;
doc.MailMerge.HideEmptyGroup = true;
Both live on MailMerge, both default to false, and both have to be turned on explicitly. The timing matters: they have to be set before Execute(), because the deletion happens during the merge, and changing the properties after the merge has finished has no effect on the result that has already been produced.
fieldNames lists only the three main fields; Optional Note, Optional Line 1, Optional Line 2 and Optional Remark are given no values, so all of them come out empty after the merge.
The Result
With both switches on, the empty paragraphs and the empty region disappear along with the merge, and the follow-up confirmation returns to a compact single-page layout:
Word output after empty paragraphs and regions have been hidden during the mail merge

Comparing the output with both switches off (the control) and with both switches on, item by item against the template:
| What the template holds | Both switches off | Both switches on |
|---|---|---|
«Optional Note» (a paragraph of its own) |
kept as one blank line | the whole paragraph is removed |
the region OptionalBlock (four paragraphs) |
kept as four blank lines | all four paragraphs are removed |
Note: «Optional Remark» |
keeps Note: |
keeps Note: |
| empty paragraphs already in the template (used for spacing) | kept | kept |
The control document has 46 lines of text in total and the result document has 41; the difference of 5 lines corresponds exactly to the paragraph holding «Optional Note» and the four paragraphs of the region OptionalBlock, while the empty lines the template already used for spacing are left untouched.
Two rules follow from this:
- The test is whether the paragraph still holds content after the merge, not whether the field received a value.
Optional Remarkin the third construct is just as empty, but the paragraph also holds the labelNote:, so the whole paragraph is kept and only the field's position is left blank. To make the label disappear together with the field, the label has to be folded into the field value. HideEmptyParagraphsis the property that actually performs the deletion. SettingHideEmptyGroup = truealone while leavingHideEmptyParagraphs = falseproduces output identical to leaving both switches off; setting onlyHideEmptyParagraphs = trueproduces output identical to turning both on.
Hide Only Empty Paragraphs Without Merge Regions
If a template only needs a line to disappear when it comes out empty and defines no merge regions, setting HideEmptyParagraphs alone is enough:
// When the template has no merge regions, this switch alone is enough
doc.MailMerge.HideEmptyParagraphs = true;
With that setting the paragraphs inside a region are still deleted one by one, so the region disappears as well, with the same result as turning both switches on. Conversely, if the template really does define regions that should be cleared as a whole, setting both properties expresses the intent more clearly.
Read the Region Names and Merge Field Names from a Word Template
The Approach
Reading the region names and the merge field list from a Word template takes three methods on MailMerge, each at a different granularity:
| Method | What it returns |
|---|---|
GetMergeGroupNames() |
every region name in the document |
GetMergeFieldNames({ groupName: "Products" }) |
the merge field names inside the named region |
GetMergeFieldNames() |
every merge field name in the document (including region markers) |
The second method receives the region name inside an object as a named argument. That is how the WebAssembly build handles an overloaded method: methods sharing a name are dispatched by argument count and argument name, so the call has to be written as { groupName: "Products" }; passing a plain string will not match that overload.
Prepare the Template
The sample template is a product and team information list that contains two merge regions, Products and TeamRanking:
«GroupStart:Products»
Owner «Contact Name»
Code «Number»
Category «Type»
Unit Price «Price»
Date «Date»
Vendor «Vendor»
«GroupEnd:Products»
«GroupStart:TeamRanking»
Participant «Name»
Points «Score»
Place «Ranking»
Award «Award»
«GroupEnd:TeamRanking»
Each field occupies a paragraph of its own, and the region markers are paragraphs of their own as well.
The Code
The steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the sample template into the virtual file system (VFS), create a
Documentobject and load the file. - Call
GetMergeGroupNames(),GetMergeFieldNames({ groupName: "Products" })andGetMergeFieldNames()in turn to read the three lists. - Join the three lists into text and write it to the VFS with
window.dotnetRuntime.Module.FS.writeFile(). - Read that text file back from the VFS, build a
text/plainBlob and trigger the browser download.
The code for reading the region names and merge field names from a Word template is as follows:
function App() {
const IdentifyMergeFieldName = 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 the sample template into the virtual file system (VFS)
let inputFileName = "IdentifyMergeFieldNames.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a document and load the template
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Read all region names
let GroupNames = doc.MailMerge.GetMergeGroupNames();
// Read the merge field names inside a specific region
let MergeFieldNamesWithinRegion = doc.MailMerge.GetMergeFieldNames({ groupName: "Products" });
// Read every merge field name in the document
let MergeFieldNames = doc.MailMerge.GetMergeFieldNames();
// Join the results into text
let content = [];
content.push("----------------Group Names-----------------------------------------\n");
for (let i = 0; i < GroupNames.length; i++) {
content.push(GroupNames[i] + "\n");
}
content.push("----------------Merge field names within a specific group-----------\n");
for (let j = 0; j < MergeFieldNamesWithinRegion.length; j++) {
content.push(MergeFieldNamesWithinRegion[j] + "\n");
}
content.push("----------------All of the merge field names------------------------\n");
for (let k = 0; k < MergeFieldNames.length; k++) {
content.push(MergeFieldNames[k] + "\n");
}
// Write the TXT file
const outputFileName = "IdentifyMergeFieldName-result.txt";
window.dotnetRuntime.Module.FS.writeFile(outputFileName, content.join("\n"));
// Close the document
doc.Close();
doc.Dispose()
// Read the saved file and convert it to a Blob
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], {type: "text/plain"});
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>Read region names and merge field names from a template</h1>
<button onClick={IdentifyMergeFieldName}>Generate</button>
</div>
);
}
export default App;
Compared with the previous example, two things are different:
Execute()is not called. The example only reads the template's structure without merging any data, so the three lists can be read straight after loading.- The output goes through
window.dotnetRuntime.Module.FS.writeFile(fileName, text), writing the string directly into the virtual file system before atext/plainBlob triggers the download. It closes withdoc.Close(), because the example never changes the document's content.
All three methods return a collection (supporting length and indexed access) and are walked in the same way as Sections and Paragraphs.
The Result
Reading the two merge regions and all of the merge field names produces the following result:
Word output after the region names and merge field names have been read from a Word template

The output file is laid out as follows (the ---…--- lines are separators):
----------------Group Names-----------------------------------------
Products
TeamRanking
----------------Merge field names within a specific group-----------
Contact Name
Number
Type
Price
Date
Vendor
----------------All of the merge field names------------------------
Products
Contact Name
Number
Type
Price
Date
Vendor
Products
TeamRanking
Name
Score
Ranking
Award
TeamRanking
The three lists come out as:
Region names (2)
Products, TeamRanking
Field names inside the Products region (6)
Contact Name, Number, Type, Price, Date, Vendor
All field names (14 entries)
Products, Contact Name, Number, Type, Price, Date, Vendor, Products,
TeamRanking, Name, Score, Ranking, Award, TeamRanking
Two points are worth noting:
GetMergeFieldNames()with no argument counts the region markers themselves as field names.GroupStart:Productsis aMERGEFIELDfield in the document, so every region name appears twice: once for the opening marker and once for the closing marker. Among the 14 entries above,ProductsandTeamRankingeach appear twice.- The version that takes
groupNamereturns only the field names inside that region; none of the six results forProductscontains aGroupStart:orGroupEnd:marker.
Why is there an extra blank line between the entries? While the text is assembled,
content.push(name + "\n")already puts a newline at the end of every entry, andcontent.join("\n")adds another one between the elements, so two newlines end up in a row. For a compact list, drop the"\n"from thepushcall:
content.push(GroupNames[i]);
Filter Region Markers Out of the Full Field List
If the full list is really needed, take the region names first and filter them out of the result of GetMergeFieldNames():
const groups = doc.MailMerge.GetMergeGroupNames();
const names = doc.MailMerge.GetMergeFieldNames();
// Collect the region names into a set
const groupSet = {};
for (let i = 0; i < groups.length; i++) groupSet[groups[i]] = true;
// Filter out the region markers, keeping only real merge field names
const realFields = [];
for (let i = 0; i < names.length; i++) {
if (!groupSet[names[i]]) realFields.push(names[i]);
}
If only the field names inside one particular region are needed, the overload that takes groupName is simpler, and its result never contains region markers.
FAQ
The switches for hiding empty lines are on and the blank line is still there
Cause: The property tests whether the paragraph still holds content after the merge. If the paragraph contains fixed text besides the field (for example, Note:), or the field itself is given a value, the whole paragraph is kept.
Solution: Check that the paragraph you want hidden holds nothing but the field. If the label should disappear together with the field, fold it into the field value: pass a complete string such as Note: … when there is data, and an empty string when there is none:
// The paragraph holds a single field; the label is carried by the field value
let fieldNames = ["Contact Name", "Remark Line"];
let fieldValues = ["John Smith", ""];
Why does setting only HideEmptyGroup = true have no effect
Cause: HideEmptyParagraphs is what actually deletes paragraphs. A region disappears as a whole because the paragraphs inside it are deleted one by one; with HideEmptyGroup turned on alone, the output is identical to leaving both switches off.
Solution: Set both properties together. If the template uses no merge regions, setting HideEmptyParagraphs alone produces the same output.
GetMergeFieldNames({ groupName: ... }) returns no data
Cause: The method is overloaded, and the WebAssembly build dispatches by argument name; passing a plain string (such as GetMergeFieldNames("Products")) does not match the overload that takes a region name.
Solution: Pass an object with a key, taking the region name from the name after GroupStart: in the template:
const fields = doc.MailMerge.GetMergeFieldNames({ groupName: "Products" });
Why do region names appear in the result of GetMergeFieldNames()
Cause: The opening and closing markers of a region are also MERGEFIELD fields in the document, so they are counted when the method is called without arguments, and every region name appears twice.
Solution: When only the field names inside one region are needed, use the overload that takes groupName; when the full list is needed, filter it using the result of GetMergeGroupNames() (see “Filter Region Markers Out of the Full Field List” above).
The template defines merge regions but GetMergeGroupNames() returns 0
Cause: The field code of a region marker is written as MERGEFIELD GroupStart:region name, with the field name unquoted. Written as MERGEFIELD "GroupStart:Products", it is usually treated as an ordinary merge field name and does not form a region. In addition, GroupStart and GroupEnd have to appear in pairs with the same region name at both ends; a marker without its counterpart is not recognised as a region.
Solution: Check these two field codes in the template:
{ MERGEFIELD GroupStart:Products \* MERGEFORMAT }
{ MERGEFIELD GroupEnd:Products \* MERGEFORMAT }
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.
Character formatting decides what text looks like on the page — which font it uses, how large it is, what color it is, and whether it is bold, slanted, underlined or struck through. In Word's Open XML structure these properties hang off the run, not the paragraph: every run in a sentence can carry a set of its own. Spire.Doc for JavaScript maps that structure onto the TextRange.CharacterFormat object and reads and writes it directly in the browser via WebAssembly, with 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 Character Formatting
After Document.AddSection() creates a section, section.AddParagraph() gives you a paragraph and paragraph.AppendText() appends text to it — the return value of AppendText is a TextRange object, and once you hold it you can set CharacterFormat on it directly. To break the line without starting a new paragraph, call paragraph.AppendBreak(wasmModule.BreakType.LineBreak).
The sample below walks through the common character formats one by one. Each line of text changes exactly one property, so the effect can be read straight off the result. One line is the exception: the font-name example deliberately mixes a Chinese word into its text, because only East Asian characters are read from the Far-East font slot that the line sets.
function App() {
const CharacterFormatting = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create the document and the section
let doc = new docModule.Document();
let sec = doc.AddSection();
// The title paragraph takes the built-in "Title" style
let titleParagraph = sec.AddParagraph();
titleParagraph.AppendText("Character Formatting Overview");
titleParagraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Title });
let paragraph = sec.AddParagraph();
// Strikeout
let tr = paragraph.AppendText("Strikethough Text");
tr.CharacterFormat.IsStrikeout = true;
// Shadow
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Shadow Text");
tr.CharacterFormat.IsShadow = true;
// Small caps
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Small caps Text");
tr.CharacterFormat.IsSmallCaps = true;
// Double strikeout
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Double Strikethough Text");
tr.CharacterFormat.DoubleStrike = true;
// Outline
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Outline Text");
tr.CharacterFormat.IsOutLine = true;
// All caps
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("AllCaps Text");
tr.CharacterFormat.AllCaps = true;
// Subscript and superscript
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Text");
tr = paragraph.AppendText("SubScript");
tr.CharacterFormat.SubSuperScript = docModule.SubSuperScript.SubScript;
tr = paragraph.AppendText("And");
tr = paragraph.AppendText("SuperScript");
tr.CharacterFormat.SubSuperScript = docModule.SubSuperScript.SuperScript;
// Emboss, with a white text color so the effect shows up better
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Emboss Text");
tr.CharacterFormat.Emboss = true;
tr.CharacterFormat.TextColor = docModule.Color.get_White();
// Hidden text
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Hidden:");
tr = paragraph.AppendText("Hidden Text");
tr.CharacterFormat.Hidden = true;
// Engrave
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Engrave Text");
tr.CharacterFormat.Engrave = true;
tr.CharacterFormat.TextColor = docModule.Color.get_White();
// The Western, non-Far-East and Far-East font names are set separately
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("WesternFonts中文字体");
tr.CharacterFormat.FontNameAscii = "Calibri";
tr.CharacterFormat.FontNameNonFarEast = "Calibri";
tr.CharacterFormat.FontNameFarEast = "Simsun";
// Font size
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Font Size");
tr.CharacterFormat.FontSize = 20;
// Font color
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Font Color");
tr.CharacterFormat.TextColor = docModule.Color.get_Red();
// Bold + italic
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Bold Italic Text");
tr.CharacterFormat.Bold = true;
tr.CharacterFormat.Italic = true;
// Underline
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Underline Style");
tr.CharacterFormat.UnderlineStyle = docModule.UnderlineStyle.Single;
// Highlight (background)
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Highlight Text");
tr.CharacterFormat.HighlightColor = docModule.Color.get_Yellow();
// Character shading
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Text has shading");
tr.CharacterFormat.TextBackgroundColor = docModule.Color.get_Green();
// Border around the characters
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Border Around Text");
tr.CharacterFormat.Border.BorderType = docModule.BorderStyle.Single;
// Character scaling at 150%
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Text Scale");
tr.CharacterFormat.TextScale = 150;
// Character spacing of 2 points
paragraph.AppendBreak(docModule.BreakType.LineBreak);
tr = paragraph.AppendText("Character Spacing is 2 point");
tr.CharacterFormat.CharacterSpacing = 2;
// Define the output file name
const outputFileName = "CharacterFormatting-result.docx";
// Save the document to the given path
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
// Read the saved file back and wrap it as a Blob
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 Various Character Formats</h1>
<button onClick={CharacterFormatting}>Generate</button>
</div>
);
}
export default App;
Every line of the generated document shows the matching character effect, and can be compared with the code line by line

Apply Several Character Formats in One Paragraph
One paragraph can hold several TextRange objects, and each of them has its own CharacterFormat. That means layouts such as "the first half in bold blue, the second half back at a regular size" do not need to be split into two paragraphs — just call AppendText again; the format only applies to the text appended by that one call.
function App() {
const MultiStylesInAParagraph = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create the document and the section
let doc = new docModule.Document();
let section = doc.AddSection();
// Add a new paragraph
let para = section.AddParagraph();
// The first TextRange: Calibri 16 pt, blue, bold, single underline
let range = para.AppendText("Quarterly Delivery Note ");
range.CharacterFormat.FontName = "Calibri";
range.CharacterFormat.FontSize = 16;
range.CharacterFormat.TextColor = docModule.Color.get_Blue();
range.CharacterFormat.Bold = true;
range.CharacterFormat.UnderlineStyle = docModule.UnderlineStyle.Single;
// The second TextRange: still the same paragraph, now Calibri 15 pt regular
range = para.AppendText("This delivery covers three business areas, and acceptance is based on the result of the online trial run");
range.CharacterFormat.FontName = "Calibri";
range.CharacterFormat.FontSize = 15;
// Define the output file name
const outputFileName = "MultiStylesInAParagraph-result.docx";
// Save the document to the given path
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
// Read the saved file back and wrap it as a Blob
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>Apply Multiple Character Formats in One Paragraph</h1>
<button onClick={MultiStylesInAParagraph}>Generate</button>
</div>
);
}
export default App;
The first half of the paragraph is bold, blue and underlined; the second half is back to a regular weight

FAQ
FontName is set, but the Chinese text does not change
Cause: CharacterFormat.FontName only writes to the Western (ascii/hAnsi) font slots, while Chinese characters are taken from the separate "East Asian" slot. The two are stored independently, so setting one leaves the other exactly as it was.
Solution: Set the font used by each script separately — FontNameAscii / FontNameNonFarEast for the Western text, FontNameFarEast for the Chinese:
tr.CharacterFormat.FontNameAscii = "Calibri";
tr.CharacterFormat.FontNameNonFarEast = "Calibri";
tr.CharacterFormat.FontNameFarEast = "Simsun";
Emboss and Engrave show no visible effect
Cause: Both effects rely on a light-and-shadow contrast, and the default black text on a white background has almost no visual difference to work with.
Solution: Set the text color to white, so that the characters are shaped by their highlight and shadow alone:
tr.CharacterFormat.Emboss = true;
tr.CharacterFormat.TextColor = wasmModule.Color.get_White();
The downloaded Word document cannot be opened, or is reported as corrupted
Cause: The MIME type was wrong when the Blob was created, so the browser cannot identify the file format correctly.
Solution: Make sure the exact docx MIME type is used:
const modifiedFile = new Blob([modifiedFileArray], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
});
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 Paragraph and List Formatting in Word with JavaScript in React
2026-10-16 07:59:18 Written by Amy ZhaoThe paragraph is the basic unit of layout in Word, and every paragraph in a document carries two formatting objects. Paragraph.Format controls how the paragraph itself is laid out — its border, alignment, shading, indentation, spacing before and after, and line spacing — while Paragraph.ListFormat controls the numbering or bullet it shows as a list item. The two are independent, so a single paragraph can be centred and indented and still be the third level of a numbered list. Spire.Doc for JavaScript reads and writes both objects directly in the browser via WebAssembly, with 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 Paragraph Formatting
Paragraph formatting is set through paragraph.Format. The sample below walks through a paragraph border, the five horizontal alignments, shading, three kinds of indentation and the spacing rules, one at a time:
function App() {
const ParagraphFormatting = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create the document and the section
let doc = new docModule.Document();
let sec = doc.AddSection();
// Title
let para = sec.AddParagraph();
para.AppendText("Paragraph Formatting Overview");
para.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Title });
// Paragraph border
para = sec.AddParagraph();
para.AppendText("This paragraph has a red border.");
para.Format.Borders.BorderType = docModule.BorderStyle.Single;
para.Format.Borders.Color = docModule.Color.get_Red();
// The five horizontal alignments: left, center, right, justify, distribute
para = sec.AddParagraph();
para.AppendText("This paragraph is left-aligned.");
para.Format.HorizontalAlignment = docModule.HorizontalAlignment.Left;
para = sec.AddParagraph();
para.AppendText("This paragraph is center-aligned.");
para.Format.HorizontalAlignment = docModule.HorizontalAlignment.Center;
para = sec.AddParagraph();
para.AppendText("This paragraph is right-aligned.");
para.Format.HorizontalAlignment = docModule.HorizontalAlignment.Right;
para = sec.AddParagraph();
para.AppendText("This paragraph is justified.");
para.Format.HorizontalAlignment = docModule.HorizontalAlignment.Justify;
para = sec.AddParagraph();
para.AppendText("This paragraph is distributed.");
para.Format.HorizontalAlignment = docModule.HorizontalAlignment.Distribute;
// Paragraph shading
para = sec.AddParagraph();
para.AppendText("This paragraph has gray shading.");
para.Format.BackColor = docModule.Color.get_Gray();
// 10 pt left indent, 10 pt right indent, 15 pt first-line indent
para = sec.AddParagraph();
para.AppendText("This paragraph has a 10 pt left indent, a 10 pt right indent and a 15 pt first-line indent.");
para.Format.SetLeftIndent(10);
para.Format.SetRightIndent(10);
para.Format.SetFirstLineIndent(15);
// A negative value passed to the first-line indent is a hanging indent
para = sec.AddParagraph();
para.AppendText("This paragraph has a hanging indent of 15 pt.");
para.Format.SetFirstLineIndent(-15);
// 10 pt before, 20 pt after, line spacing of at least 10 pt
para = sec.AddParagraph();
para.AppendText("This paragraph has 10 pt before, 20 pt after and a line spacing of at least 10 pt.");
para.Format.AfterSpacing = 20;
para.Format.BeforeSpacing = 10;
para.Format.LineSpacingRule = docModule.LineSpacingRule.AtLeast;
para.Format.LineSpacing = 10;
// Define the output file name
const outputFileName = "ParagraphFormatting-result.docx";
// Save the document to the given path
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
// Release resources
doc.Dispose();
// Read the saved file back and wrap it as a Blob
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 Various Paragraph Formats</h1>
<button onClick={ParagraphFormatting}>Generate</button>
</div>
);
}
export default App;
Each paragraph in the generated document shows its border, alignment, indentation and spacing in turn

Create and Apply List Styles
A list style is one kind of document style, created with document.Styles.Add({ listType, name }). listType is ListType.Numbered or ListType.Bulleted, and name is the custom style name you choose.
A new style also needs the numbering rule for each level. Those settings live in ListRef.Levels and are indexed from 0:
NumberPrefix— the prefix template for the number, where%1stands for the first level's counter,%2for the second, and so on;PatternType— how the number is rendered, e.g.ListPatternType.Arabicfor Arabic numerals;- on the paragraph side,
ListFormat.ApplyStyle(styleName)attaches the list style, andListFormat.ListLevelNumbersets which level the paragraph sits at (0 is the first level).
function App() {
const Lists = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create the document and the section
const doc = new docModule.Document();
let sec = doc.AddSection();
// The title and the group headings
let paragraph = sec.AddParagraph();
paragraph.AppendText("List Styles Overview");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Title });
paragraph = sec.AddParagraph();
paragraph.AppendText("Numbered list:").CharacterFormat.Bold = true;
// Create the numbered list style and configure the numbering for levels 1 and 2
let numberList = doc.Styles.Add({ listType: docModule.ListType.Numbered, name: "numberList" });
numberList.ListRef.Levels.get_Item(1).NumberPrefix = "%1.";
numberList.ListRef.Levels.get_Item(1).PatternType = docModule.ListPatternType.Arabic;
numberList.ListRef.Levels.get_Item(2).NumberPrefix = "%1.%2.";
numberList.ListRef.Levels.get_Item(2).PatternType = docModule.ListPatternType.Arabic;
// Create the bulleted list style
let bulletList = doc.Styles.Add({ listType: docModule.ListType.Bulleted, name: "bulletList" });
// Add both list styles to the document's style collection
doc.Styles.Add(numberList);
doc.Styles.Add(bulletList);
// Apply the numbered list style paragraph by paragraph, setting the level with ListLevelNumber
paragraph = sec.AddParagraph();
paragraph.AppendText("Overview");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph = sec.AddParagraph();
paragraph.AppendText("Delivery Scope");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph = sec.AddParagraph();
paragraph.AppendText("Three Business Areas");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph.ListFormat.ListLevelNumber = 1;
paragraph = sec.AddParagraph();
paragraph.AppendText("Data Ingestion");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph.ListFormat.ListLevelNumber = 1;
paragraph = sec.AddParagraph();
paragraph.AppendText("API Integration");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph.ListFormat.ListLevelNumber = 2;
paragraph = sec.AddParagraph();
paragraph.AppendText("Field Verification");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph.ListFormat.ListLevelNumber = 2;
paragraph = sec.AddParagraph();
paragraph.AppendText("Canary Release");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph.ListFormat.ListLevelNumber = 2;
paragraph = sec.AddParagraph();
paragraph.AppendText("Acceptance Criteria");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph.ListFormat.ListLevelNumber = 1;
paragraph = sec.AddParagraph();
paragraph.AppendText("Follow-up Plan");
paragraph.ListFormat.ApplyStyle(numberList.Name);
// The bulleted list
paragraph = sec.AddParagraph();
paragraph.AppendText("Bulleted list:").CharacterFormat.Bold = true;
paragraph = sec.AddParagraph();
paragraph.AppendText("Clause One");
paragraph.ListFormat.ApplyStyle(bulletList.Name);
paragraph = sec.AddParagraph();
paragraph.AppendText("Clause Two");
paragraph.ListFormat.ApplyStyle(bulletList.Name);
paragraph = sec.AddParagraph();
paragraph.AppendText("Supplementary Notes");
paragraph.ListFormat.ApplyStyle(bulletList.Name);
paragraph.ListFormat.ListLevelNumber = 1;
paragraph = sec.AddParagraph();
paragraph.AppendText("Exceptions");
paragraph.ListFormat.ApplyStyle(bulletList.Name);
paragraph.ListFormat.ListLevelNumber = 1;
paragraph = sec.AddParagraph();
paragraph.AppendText("Clause Three");
paragraph.ListFormat.ApplyStyle(bulletList.Name);
// Define the output file name
const outputFileName = "Lists-result.docx";
// Save the document to the given path
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
// Read the saved file back and wrap it as a Blob
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>Create and Apply List Styles</h1>
<button onClick={Lists}>Generate</button>
</div>
);
}
export default App;
The numbered list generates 1. / 1.1. style counters automatically, while the bulleted list indents by level

FAQ
The hanging indent does not take effect
Cause: A negative value passed to SetFirstLineIndent means a hanging indent, but if the paragraph has no left indent of its own there is no room for the hanging part to pull back into, so the effect is almost invisible.
Solution: Give the paragraph a left indent first, then use a negative first-line indent for the hanging effect:
para.Format.SetLeftIndent(20);
para.Format.SetFirstLineIndent(-15);
The list numbers do not appear at the expected level
Cause: ListLevelNumber defaults to 0 (the first level) when it is not set, so a child paragraph that does not set its level explicitly is numbered alongside its parent.
Solution: Set the level explicitly on every paragraph that needs one, keeping in mind that it is also counted from 0:
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph.ListFormat.ListLevelNumber = 1; // Second level, numbered 1.1.
A paragraph has both shading and a border, but only one of them shows
Cause: Format.BackColor is paragraph shading and Format.Borders is the paragraph border. Both are written under the same paragraph property node, so a misspelled property name is silently ignored.
Solution: Check which property belongs where — shading goes on BackColor, the border on Borders.BorderType:
para.Format.BackColor = docModule.Color.get_Gray();
para.Format.Borders.BorderType = docModule.BorderStyle.Single;
para.Format.Borders.Color = docModule.Color.get_Red();
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.
Insert Content based on Word Bookmarks with JavaScript in React
2026-10-16 07:58:52 Written by Amy ZhaoA bookmark is a named anchor that marks a specific position or a range of content in a Word document. Writing content into a bookmark lets business data land at the designated place in a template automatically, without the caller having to know how the document is actually laid out. Spire.Doc for JavaScript performs this 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 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.
Insert Document Content at a Bookmark
Inserting document content into a bookmark runs in three stages: first, instantiate two Document objects and load a file into each, one for the document to be filled and one that provides the content; then locate the bookmark by name with BookmarksNavigator.MoveToBookmark, take the bookmark's start marker through CurrentBookmark.BookmarkStart, work out the index of the paragraph holding that bookmark within the section through its OwnerParagraph and the section's IndexOf, and insert every paragraph of the source document after that index (each one Clone()d first); last, save the document, read the generated file from VFS, wrap it as a Blob and trigger a browser download.
function App() {
const InsertDocAtBookmark = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the target document to be filled into VFS
let inputFileName_1 = "InsertDocAtBookmark1.docx";
await window.spire.FetchFileToVFS(inputFileName_1, "", `${process.env.PUBLIC_URL}/static/data/`);
// Load the source document that provides the content into VFS
let inputFileName_2 = "InsertDocAtBookmark2.docx";
await window.spire.FetchFileToVFS(inputFileName_2, "", `${process.env.PUBLIC_URL}/static/data/`);
// Load the target document
let document1 = new docModule.Document();
document1.LoadFromFile(inputFileName_1);
// Load the source document
let document2 = new docModule.Document();
document2.LoadFromFile(inputFileName_2);
// Get the first section of the target document
let section1 = document1.Sections.get_Item();
// Create a bookmark navigator to locate the bookmark
let bn = new docModule.BookmarksNavigator(document1);
// Find the bookmark by name
bn.MoveToBookmark("Test", true, true);
// Get the bookmark's start marker
let start = bn.CurrentBookmark.BookmarkStart;
// Get the paragraph that holds the start marker
let para = start.OwnerParagraph;
// Get the index of that paragraph in the section
let index = section1.Body.ChildObjects.IndexOf(para);
// Insert the paragraphs of the source document one by one after the paragraph holding the bookmark
for (let i = 0; i < document2.Sections.Count; i++) {
let section2 = document2.Sections.get_Item(i);
for (let j = section2.Paragraphs.Count-1; j >=0; j--) {
let paragraph = section2.Paragraphs.get_Item(j);
section1.Body.ChildObjects.Insert(index + 1, paragraph.Clone());
}
}
// Define the output file name
const outputFileName = "InsertDocAtBookmark-result.docx";
// Save the document
document1.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
document1.Dispose();
document2.Dispose();
// Read the saved file and convert it to a Blob
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 a document at a Word bookmark</h1>
<button onClick={InsertDocAtBookmark}>Generate</button>
</div>
);
}
export default App;
Word output after the paragraphs of the source document have been inserted at the bookmark

Insert an Image at a Bookmark
A bookmark marks a position in the document, so it can carry non-text content just as well — inserting a seal image at a signature bookmark in a contract template, for example. The core process runs in three stages: first, instantiate a Document and load the file; then locate the bookmark with a BookmarksNavigator, create a temporary section with AddSection, add a paragraph inside that temporary section and append the image to it with AppendPicture, insert the whole paragraph at the bookmark with InsertParagraph, and finally remove the temporary section; last, save the document, read the generated file from VFS, wrap it as a Blob and trigger a browser download.
function App() {
const InsertImageAtBookmark = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the target document into VFS
let inputFileName = "InsertImageAtBookmark.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/static/data/`);
// Load the image to be inserted into VFS
let imageFileName = "Word.png";
await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Create a bookmark navigator to locate the bookmark
let bn = new docModule.BookmarksNavigator(doc);
// Find the bookmark by name
bn.MoveToBookmark("Test", true, true);
// Add a temporary section to hold the image paragraph
let section0 = doc.AddSection();
// Add a paragraph in the temporary section
let paragraph = section0.AddParagraph();
// Append the image to the paragraph
paragraph.AppendPicture({ imgFile: imageFileName });
// Insert that paragraph at the bookmark
bn.InsertParagraph(paragraph);
// Remove the temporary section
doc.Sections.Remove(section0);
// Define the output file name
const outputFileName = "InsertImageAtBookmark-result.docx";
// Save the document
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
doc.Dispose();
// Read the saved file and convert it to a Blob
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 at a Word bookmark</h1>
<button onClick={InsertImageAtBookmark}>Generate</button>
</div>
);
}
export default App;
Word output after the image has been inserted at the bookmark

FAQ
The bookmark cannot be found and MoveToBookmark fails to locate it
Cause: MoveToBookmark locates a bookmark by its name. The name is case-sensitive and has to match the bookmark name in the document exactly. If the spelling does not match, or the target document has no bookmark with that name at all, the lookup fails.
Solution: Before locating, walk the Bookmarks collection to confirm the bookmark names the document actually carries, then pass the exact name:
// Walk all the bookmarks in the document to check whether the name matches
for (let i = 0; i < doc.Bookmarks.Count; i++) {
console.log(doc.Bookmarks.get_Item(i).Name);
}
// Once the bookmark is confirmed to exist, locate it by its exact name
let bn = new wasmModule.BookmarksNavigator(doc);
bn.MoveToBookmark("Test", true, true);
The inserted image is larger than the page
Cause: AppendPicture inserts the image at its original pixel size and does not fit it to the page width. If the image resolution is far above the available page width, it overflows the page or stretches the original layout once inserted.
Solution: AppendPicture returns the picture object, so it can be scaled proportionally to a maximum width before it is inserted at the bookmark (Width and Height are measured in points):
// Append the image and keep the returned picture object
let paragraph = section0.AddParagraph();
let picture = paragraph.AppendPicture({ imgFile: imageFileName });
// Scale proportionally to the maximum width
let maxWidth = 400;
if (picture.Width > maxWidth) {
picture.Height = picture.Height * (maxWidth / picture.Width);
picture.Width = maxWidth;
}
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.
Retrieve Style Information from a Word Document with JavaScript in React
2026-09-30 08:40:53 Written by Amy ZhaoA well-formatted document is worth more than its looks: the style a paragraph carries is structured information in its own right. Whether a heading paragraph is on Heading 1 or Heading 2, and which style the body copy uses, both say something about how the document is organised. The same works in reverse — when the headings of a document have to be pulled out to build a table of contents or a summary, the reliable way is to select them by style name rather than to guess which line looks bigger.
In Spire.Doc for JavaScript every paragraph carries a StyleName property, and reading it returns the style that paragraph is currently using. Both examples in this article write their output to a TXT file.
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.
Export the Style Names of a Document
The paragraphs of a document are organised in two levels, section then paragraph: doc.Sections is the collection of sections, and each section's Paragraphs is the collection of paragraphs. Walk both with Count + get_Item(index), read the StyleName of each paragraph and join the results up.
function App() {
const RetrieveStyle = 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 = "RetrieveStyle.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Walk every section -> paragraph and read each paragraph's StyleName
let styleName = "";
for (let i = 0; i < doc.Sections.Count; i++) {
let section = doc.Sections.get_Item(i);
for (let j = 0; j < section.Paragraphs.Count; j++) {
let paragraph = section.Paragraphs.get_Item(j);
styleName += paragraph.StyleName + "\r\n";
}
}
// Define the output file name
const outputFileName = "RetrieveStyle-result.txt";
// Write the content to the TXT file (straight into the VFS, not through SaveToFile)
window.dotnetRuntime.Module.FS.writeFile(outputFileName, styleName);
doc.Close();
// Read the written file and wrap it in a Blob
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: "text/plain" });
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>Read the Style Name of Every Paragraph</h1>
<button onClick={RetrieveStyle}>Generate</button>
</div>
);
}
export default App;
The sample document holds 14 paragraphs, and the exported TXT lists one style name per line, in paragraph order:
Title
Heading1
Normal
Heading2
Normal
Heading1
Normal
Heading2
Normal
Heading1
Normal
ListBullet
ListBullet
ListBullet
Note that the built-in style names come back without the space — Heading1 rather than Heading 1, ListBullet rather than List Bullet. That matters a great deal once a style name is used in a comparison.
The style name list exported for each paragraph of the sample document

Extract Paragraph Text by Style Name
The same two-level walk applies; all it takes is one extra test per paragraph — only when StyleName equals the target style name does paragraph.Text go into the result. The sample document holds 3 Heading1 paragraphs, with a Title, Heading 2 and body paragraphs around them, which shows that the filter really does pick out the target style alone.
function App() {
const GetTextByStyleName = 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 = "GetTextByStyleName.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Load the document
let doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Collect the text of the paragraphs that match
let builder = [];
// Walk every section -> paragraph
for (let i = 0; i < doc.Sections.Count; i++) {
let section = doc.Sections.get_Item(i);
for (let j = 0; j < section.Paragraphs.Count; j++) {
let para = section.Paragraphs.get_Item(j);
// Take only the paragraphs whose style name is Heading1
if (para.StyleName == "Heading1") {
builder.push(para.Text);
}
}
}
// Define the output file name
const outputFileName = "GetTextByStyleName-result.txt";
// Write the content to the TXT file
window.dotnetRuntime.Module.FS.writeFile(outputFileName, builder.join("\n"));
doc.Close();
// Read the written file and wrap it in a Blob
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: "text/plain" });
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>Extract Paragraph Text by Style Name</h1>
<button onClick={GetTextByStyleName}>Generate</button>
</div>
);
}
export default App;
The exported TXT holds just 3 lines, which are exactly the Heading1 paragraphs of the document:
1. Review of Last Week
2. This Week’s Priorities
3. Items Awaiting Confirmation
Only the paragraphs on the target style reached the exported TXT

FAQ
Filtering by style name matches no paragraph at all
Cause: The StyleName of a built-in style is the space-free form, so writing "Heading 1" matches nothing.
Solution: Use the name without the space. In the sample, "Heading1" matches 3 paragraphs while "Heading 1" matches 0:
// Right
if (para.StyleName == "Heading1") { ... }
// Matches nothing
if (para.StyleName == "Heading 1") { ... }
Names are case-sensitive as well, so run the style-name export first to confirm the names the document actually uses before writing the condition.
The exported TXT has a blank line between every line
Cause: The text is joined both ways at once — push(text + "\n") and then join("\n"). Every element already carries a newline and join adds another one, so a blank line appears between every pair.
Solution: Keep only one of the two — either leave the newline off the elements and let join add it, or keep it on the elements and join with join(""):
// Let join supply the line breaks
builder.push(para.Text);
window.dotnetRuntime.Module.FS.writeFile(outputFileName, builder.join("\n"));
The downloaded file opens as garbled text
Cause: The MIME type of the Blob is set to docx, or the text is treated as a binary document when it is read back, so opening it in Word naturally shows nonsense.
Solution: A TXT output has to declare text/plain, and it should be opened in a text editor such as Notepad:
const modifiedFile = new Blob([modifiedFileArray], { type: "text/plain" });
Paragraphs inside tables are missed when walking Sections and Paragraphs
Cause: section.Paragraphs holds the body paragraphs only — paragraphs inside table cells are not in it. Table content hangs off section.Tables, and the Paragraphs of every cell has to be walked separately.
Solution: If the document contains tables and they need to be processed too, walk them outside the body loop:
for (let t = 0; t < section.Tables.Count; t++) {
let table = section.Tables.get_Item(t);
for (let r = 0; r < table.Rows.Count; r++) {
for (let c = 0; c < table.Rows.get_Item(r).Cells.Count; c++) {
let cell = table.Rows.get_Item(r).Cells.get_Item(c);
// cell.Paragraphs holds the paragraphs inside the cell
}
}
}
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.
Restart List Numbering and Customize Bullets in Word with JavaScript in React
2026-09-30 08:40:13 Written by Amy ZhaoA consecutive sequence is often all a list needs — but real documents regularly ask for a new group that starts at a chosen number, or for a more distinctive glyph than the default dot. Neither change touches the paragraphs: both are level parameters of the list style itself, and both hang off ListRef.Levels — StartAt for the starting number, BulletCharacter for the glyph. Spire.Doc for JavaScript reads and writes these parameters directly in the browser via WebAssembly, with 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.
Restart a List at a Specific Number
When two lists share one style object, Word treats them as a single numbering sequence and the second group simply carries on from the first. To make the second group count again, the most direct approach is to create a separate list style for it and set the StartAt property of level 0 on that style — it decides which number the level starts at.
StartAt follows the same indexing as ListLevelNumber: get_Item(0) is the first level. The sample sets the second list to 10, so its first entry renders as 10.:
function App() {
const RestartList = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create the document and the section
let doc = new docModule.Document();
let section = doc.AddSection();
// Group heading
let paragraph = section.AddParagraph();
paragraph.AppendText("Group One");
// The first list style: starts at 1 by default
let numberList = doc.Styles.Add({ listType: docModule.ListType.Numbered, name: "Numbered1" });
doc.Styles.Add(numberList);
// Apply the style paragraph by paragraph
paragraph = section.AddParagraph();
paragraph.AppendText("Item One");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph = section.AddParagraph();
paragraph.AppendText("Item Two");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph = section.AddParagraph();
paragraph.AppendText("Item Three");
paragraph.ListFormat.ApplyStyle(numberList.Name);
paragraph = section.AddParagraph();
paragraph.AppendText("Item Four");
paragraph.ListFormat.ApplyStyle(numberList.Name);
// Group heading
paragraph = section.AddParagraph();
paragraph.AppendText("Group Two");
// The second list style: set the starting number of level 0 to 10
let numberList2 = doc.Styles.Add({ listType: docModule.ListType.Numbered, name: "Numbered2" });
numberList2.ListRef.Levels.get_Item(0).StartAt = 10;
doc.Styles.Add(numberList2);
// Apply the second style paragraph by paragraph; the numbering starts at 10
paragraph = section.AddParagraph();
paragraph.AppendText("Item Five");
paragraph.ListFormat.ApplyStyle(numberList2.Name);
paragraph = section.AddParagraph();
paragraph.AppendText("Item Six");
paragraph.ListFormat.ApplyStyle(numberList2.Name);
paragraph = section.AddParagraph();
paragraph.AppendText("Item Seven");
paragraph.ListFormat.ApplyStyle(numberList2.Name);
paragraph = section.AddParagraph();
paragraph.AppendText("Item Eight");
paragraph.ListFormat.ApplyStyle(numberList2.Name);
// Define the output file name
const outputFileName = "RestartList-result.docx";
// Save the document to the given 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>Restart the Second List at a Given Number</h1>
<button onClick={RestartList}>Generate</button>
</div>
);
}
export default App;
The first list is numbered 1 to 4, the second starts at 10 and runs 10 to 13

Customize the Glyph a Bullet Displays
A bulleted list uses a dot such as · by default. Switching it to another shape takes two properties:
BulletCharacter— the character itself. It is a single character and can be produced from an ASCII/Unicode code point withString.fromCharCode();CharacterFormat.FontName— the font that carries the character. The shape of a bullet is really decided by the font: one code point draws a different figure under different fonts.
The second point is the one that matters. Symbol fonts such as Wingdings map ordinary letters onto geometric shapes, so a single character code comes out completely different under Wingdings than under a regular font. The sample below builds four list styles from four code points and applies each of them to the same sentence, so that the differences can be compared side by side:
function App() {
const ASCIICharactersBulletStyle = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Create the document and the section
let doc = new docModule.Document();
let section = doc.AddSection();
// Spell the glyph out as a character code and set the font that carries it to Wingdings
let listStyle1 = doc.Styles.Add({ listType: docModule.ListType.Bulleted, name: "liststyle" });
listStyle1.ListRef.Levels.get_Item(0).BulletCharacter = String.fromCharCode(0x006e);
listStyle1.ListRef.Levels.get_Item(0).CharacterFormat.FontName = "Wingdings";
let listStyle2 = doc.Styles.Add({ listType: docModule.ListType.Bulleted, name: "liststyle2" });
listStyle2.ListRef.Levels.get_Item(0).BulletCharacter = String.fromCharCode(0x0075);
listStyle2.ListRef.Levels.get_Item(0).CharacterFormat.FontName = "Wingdings";
let listStyle3 = doc.Styles.Add({ listType: docModule.ListType.Bulleted, name: "liststyle3" });
listStyle3.ListRef.Levels.get_Item(0).BulletCharacter = String.fromCharCode(0x00b2);
listStyle3.ListRef.Levels.get_Item(0).CharacterFormat.FontName = "Wingdings";
let listStyle4 = doc.Styles.Add({ listType: docModule.ListType.Bulleted, name: "liststyle4" });
listStyle4.ListRef.Levels.get_Item(0).BulletCharacter = String.fromCharCode(0x00d8);
listStyle4.ListRef.Levels.get_Item(0).CharacterFormat.FontName = "Wingdings";
// Four paragraphs share one sentence, each with one of the four list styles
let p1 = section.Body.AddParagraph();
p1.AppendText("Same text, four different bullet characters");
p1.ListFormat.ApplyStyle(listStyle1.Name);
let p2 = section.Body.AddParagraph();
p2.AppendText("Same text, four different bullet characters");
p2.ListFormat.ApplyStyle(listStyle2.Name);
let p3 = section.Body.AddParagraph();
p3.AppendText("Same text, four different bullet characters");
p3.ListFormat.ApplyStyle(listStyle3.Name);
let p4 = section.Body.AddParagraph();
p4.AppendText("Same text, four different bullet characters");
p4.ListFormat.ApplyStyle(listStyle4.Name);
// Define the output file name
const outputFileName = "ASCIICharactersBulletStyle-result.docx";
// Save the document to the given 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>Create a Bullet Style from ASCII Characters</h1>
<button onClick={ASCIICharactersBulletStyle}>Generate</button>
</div>
);
}
export default App;
Four identical lines, each with a different bullet; the difference comes from the BulletCharacter code point

FAQ
StartAt is set, but the list still continues from the previous group
Cause: The new list and the old one reuse the same style object. Every reference to one style shares a single numbering sequence, so changing StartAt moves the starting point of the whole sequence.
Solution: Add a separate style for the group that has to count again, and set StartAt on that new style:
let numberList2 = document.Styles.Add({ listType: wasmModule.ListType.Numbered, name: "Numbered2" });
numberList2.ListRef.Levels.get_Item(0).StartAt = 10;
The custom bullet shows up as a box or as garbled text
Cause: Only BulletCharacter was set and the font was left alone — or a regular font that does not contain the glyph was used. The system cannot render the code point and falls back to the missing-glyph box.
Solution: Set the symbol font to one that really contains the glyph (Wingdings, for example):
listStyle1.ListRef.Levels.get_Item(0).BulletCharacter = String.fromCharCode(0x006e);
listStyle1.ListRef.Levels.get_Item(0).CharacterFormat.FontName = "Wingdings";
The list level parameter was changed but nothing happens
Cause: ListRef.Levels is indexed from 0. Writing 1 out of habit changes the second level instead, and the first level stays exactly as it was.
Solution: Check the index — get_Item(0) is the first level:
// First level
numberList.ListRef.Levels.get_Item(0).StartAt = 10;
// Second level
numberList.ListRef.Levels.get_Item(1).NumberPrefix = "%1.";
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.
The font name, the font size and the text color are the most basic character-level formatting properties a Word document has, and also the ones you reach for most often: normalizing the font across a whole document, giving the headings a more striking face, or marking a key conclusion in a color that stands out. Spire.Doc for JavaScript performs all of this 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.
Set the Font
Setting the font involves three steps: 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, create a CharacterFormat object and set its FontName and FontSize, walk through the child objects of the target paragraph and call ApplyCharacterFormat on every object whose type is TextRange; finally, read the saved file back from VFS, wrap it as a Blob and trigger a browser download.
One detail deserves attention: the CharacterFormat constructor must be passed the Document instance the format belongs to. That is the convention shared by every standalone format object in Spire.Doc.
function App() {
const SetFont = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the font file into VFS
await window.spire.FetchFileToVFS("ARIALUNI.TTF", "/Library/Fonts/", `${process.env.PUBLIC_URL}static/font/`);
// Load the target Word document into VFS
const inputFileName = "SetFont.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a Document instance and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Get the second paragraph of the first section
const p = doc.Sections.get_Item(0).Paragraphs.get_Item(1);
// Create a CharacterFormat and set the font name and the font size
const format = new docModule.CharacterFormat(doc);
format.FontName = "Arial Unicode MS";
format.FontSize = 16;
// Walk through the child objects of the paragraph and apply the character format to the text ranges
for (let i = 0; i < p.ChildObjects.Count; i++) {
const childObj = p.ChildObjects.get_Item(i);
if (childObj instanceof docModule.TextRange) {
childObj.ApplyCharacterFormat(format);
}
}
// Define the output file name
const outputFileName = "SetFont_out.docx";
// Save the document to VFS
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 the Font of a Word Document</h1>
<button onClick={SetFont}>Generate</button>
</div>
);
}
export default App;
The document produced after the font has been set

Change the Font Color
Changing the font color follows the same three steps as setting the font. The difference is that there is no need to create a separate CharacterFormat object: the color is a single property, so you assign to CharacterFormat.TextColor of the TextRange itself. The color values come from the predefined properties of Color, whose names match .NET's KnownColor — get_RosyBrown() and get_DarkGreen(), for example; when you need an exact color value, use Color.FromArgb(r, g, b) instead.
function App() {
const ChangeFontColor = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the target Word document into VFS
const inputFileName = "ChangeFontColor.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a Document instance and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Get the first section
const section = doc.Sections.get_Item(0);
// Turn the text of the first paragraph rosy brown
const p1 = section.Paragraphs.get_Item(0);
for (let i = 0; i < p1.ChildObjects.Count; i++) {
const childObj = p1.ChildObjects.get_Item(i);
if (childObj instanceof docModule.TextRange) {
childObj.CharacterFormat.TextColor = docModule.Color.get_RosyBrown();
}
}
// Turn the text of the second paragraph dark green
const p2 = section.Paragraphs.get_Item(1);
for (let i = 0; i < p2.ChildObjects.Count; i++) {
const childObj = p2.ChildObjects.get_Item(i);
if (childObj instanceof docModule.TextRange) {
childObj.CharacterFormat.TextColor = docModule.Color.get_DarkGreen();
}
}
// Define the output file name
const outputFileName = "ChangeFontColor_out.docx";
// Save the document to VFS
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>Change the Font Color of a Word Document</h1>
<button onClick={ChangeFontColor}>Generate</button>
</div>
);
}
export default App;
The document produced after the font color has been changed

FAQ
Only a few words in a paragraph need to change, not the whole paragraph
Cause: The examples above treat a paragraph as the smallest unit — they walk through every child object of the paragraph and assign unconditionally to anything that is a TextRange, so once a paragraph has been processed the whole paragraph ends up in the same color or the same font. To work at word level you first have to locate the TextRange that holds the target text, and then set its character format on its own.
Solution: Use FindAllString to find the text in the document. Every TextSelection it returns gives you the matching TextRange through GetAsOneRange(), so you can change just that one occurrence:
// Find every occurrence of "key conclusion"; the arguments are: search text, case sensitivity, whole-word matching
const selections = doc.FindAllString("key conclusion", false, true);
for (let i = 0; i < selections.length; i++) {
// Only this short run of text gets the new color; the rest of the paragraph is untouched
selections[i].GetAsOneRange().CharacterFormat.TextColor = docModule.Color.get_Red();
}
If only the first occurrence matters, replace FindAllString with FindString, which returns a single TextSelection object.
The font is set, but the document shows a different font on another computer
Cause: FontName only writes the font name into the character properties of the document; it does not bring the font file itself along with it. The font loaded by FetchFileToVFS only serves the rendering and the text measurement of this browser session. When the document is opened in Word on another machine and that font is not installed there, Word falls back to another font according to its own font substitution table, and the font size and the line spacing can change with it.
Solution: If the target font is a common one (Arial or Times New Roman, for example), there is usually nothing to do. If a particular font has to be used, embed the font file into the document together with the content, so that the document still renders correctly on machines where the font is not installed. See "Embed a Private Font" in the next article for the details.
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.
Get Used Fonts and Embed Private Fonts in Word with JavaScript in React
2026-09-30 08:16:24 Written by Amy ZhaoWhen a Word document has to travel between machines and environments, fonts are the part most likely to break: open a carefully laid out document on another computer and the glyph shapes, the font sizes and even the pagination can change. The safe approach is to first take stock of the fonts the document actually uses, then embed the fonts that must be preserved together with the document itself. Spire.Doc for JavaScript performs all of this 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.
Get the List of Fonts Used in the Document
Listing the fonts involves three steps: first, load the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the document, walk down through section → paragraph → child object and read the font name, the font size and the text color from the CharacterFormat of every TextRange, de-duplicating on the combination of the three; finally, join the results into text and write it to VFS, read it back and wrap it as a Blob to trigger a browser download.
The de-duplication step deserves attention: a Map key has to be a primitive type that can be compared by value. Use an object such as { size, name } as the key and every iteration creates a brand-new reference, so Map never finds an existing entry and the de-duplication silently does nothing. The key here is therefore a string built from the font name, the font size and the color.
function App() {
const GetListOfUsingFonts = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the target Word document into VFS
const inputFileName = "GetListOfUsingFonts.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a Document instance and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// De-duplicate on "font name|size|color"
const fontMap = new Map();
// Walk through every section
for (let i = 0; i < doc.Sections.Count; i++) {
const section = doc.Sections.get_Item(i);
// Walk through every paragraph in the section
for (let j = 0; j < section.Body.Paragraphs.Count; j++) {
const paragraph = section.Body.Paragraphs.get_Item(j);
// Walk through every child object in the paragraph
for (let k = 0; k < paragraph.ChildObjects.Count; k++) {
const obj = paragraph.ChildObjects.get_Item(k);
if (!(obj instanceof docModule.TextRange)) continue;
const format = obj.CharacterFormat;
const key = `${format.FontName}|${format.FontSize}|${format.TextColor.Name}`;
fontMap.set(key, {
name: format.FontName,
size: format.FontSize,
color: format.TextColor.Name
});
}
}
}
// Build the output content
const lines = [];
for (const font of fontMap.values()) {
lines.push(`Font Name: ${font.name}, Size: ${font.size}, Color: ${font.color}`);
}
// Define the output file name and write it to VFS
const outputFileName = "GetListOfUsingFonts_out.txt";
window.dotnetRuntime.Module.FS.writeFile(outputFileName, lines.join("\n"));
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([modifiedFileArray], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
doc.Dispose();
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Get the Fonts Used in a Word Document</h1>
<button onClick={GetListOfUsingFonts}>Generate</button>
</div>
);
}
export default App;
The text file produced by the font list sample

Embed a Private Font
Embedding a private font also involves three steps: first, load the font file to embed together with the document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the document, write a run that uses the font, set EmbedFontsInFile to true and call AddPrivateFont to register the path and the registration name of the font file; finally, save the document, read it back from VFS and wrap it as a Blob to trigger a browser download.
EmbedFontsInFile and AddPrivateFont only take effect at save time and must both be called before SaveToFile. The first argument of PrivateFontPath is the name the font is registered under inside the document, and it has to match the value assigned to CharacterFormat.FontName exactly, or Word will not find a match.
function App() {
const EmbedPrivateFont = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the private font file to embed into VFS
await window.spire.FetchFileToVFS("PT Serif Caption.ttf", "/Library/Fonts/", `${process.env.PUBLIC_URL}static/font/`);
// Load the target Word document into VFS
const inputFileName = "EmbedPrivateFont.docx";
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}static/data/`);
// Create a Document instance and load the document
const doc = new docModule.Document();
doc.LoadFromFile(inputFileName);
// Append a paragraph at the end of the first section and apply the private font
const p = doc.Sections.get_Item(0).AddParagraph();
const range = p.AppendText("Quarterly Operations Review");
range.CharacterFormat.FontName = "PT Serif Caption";
range.CharacterFormat.FontSize = 20;
// Turn on font embedding and register the private font file with the document
doc.EmbedFontsInFile = true;
doc.AddPrivateFont(new docModule.PrivateFontPath("PT Serif Caption", "PT Serif Caption.ttf"));
// Define the output file name
const outputFileName = "EmbedPrivateFont_out.docx";
// Save the document to VFS
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>Embed a Private Font in a Word Document</h1>
<button onClick={EmbedPrivateFont}>Generate</button>
</div>
);
}
export default App;
The document produced after the private font has been embedded

FAQ
The same font appears many times in the output font list
Cause: If the code follows the pattern "store the font name and the font size in an object, then use that object as the Map key", the de-duplication never works. Objects are reference types in JavaScript and Map compares keys by reference rather than by value; every object created inside the loop is a fresh reference, so fontMap.has(font) always returns false. The list then ends up with one line for every TextRange in the document, and the same font repeats over and over.
Solution: Use a primitive type as the key instead — join the fields that take part in the de-duplication into a string (such as font name|size|color), so that fonts with identical content get the same key:
const format = obj.CharacterFormat;
// Use a string as the key: identical content means the same entry, so de-duplication works
const key = `${format.FontName}|${format.FontSize}|${format.TextColor.Name}`;
fontMap.set(key, {
name: format.FontName,
size: format.FontSize,
color: format.TextColor.Name
});
If all you need is de-duplication at the level of font names, collect format.FontName in a Set instead.
The font is embedded, but another computer still shows a substitute
Cause: There are three common cases. The first is the timing of the call — AddPrivateFont only registers the font file with the in-memory document object, while the font table and the font parts are actually written into the docx during the save, so EmbedFontsInFile = true and AddPrivateFont(...) both have to come before SaveToFile; leave out EmbedFontsInFile and the font file is not written at all. The second is a font name mismatch — the first argument of PrivateFontPath is the registration name used inside the document and must match the value of CharacterFormat.FontName exactly, spaces and letter case included, or the match fails over a single character. The third is the embedding permission of the font file itself — the fsType bits in the OS/2 table of some commercial fonts forbid embedding, and Word simply ignores those fonts, in which case the only options are to use another font or a licensed version that permits embedding.
Solution: Keep every font embedding setting together before the save, and make sure the registration name matches the font name character for character:
// Keep the font name and the registration name exactly the same
const FONT_NAME = "PT Serif Caption";
const range = p.AppendText("Quarterly Operations Review");
range.CharacterFormat.FontName = FONT_NAME;
range.CharacterFormat.FontSize = 20;
// Turn on embedding and register the font file; both must come before SaveToFile
doc.EmbedFontsInFile = true;
doc.AddPrivateFont(new docModule.PrivateFontPath(FONT_NAME, "PT Serif Caption.ttf"));
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
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.
Create, Modify and Copy Word Styles with JavaScript in React
2026-09-30 08:15:23 Written by Amy ZhaoStyles are the way Word reuses formatting: give a set of formatting — the font, the size, the colour, the paragraph spacing — a name and save it, and from then on every paragraph that carries that style name picks the whole set up automatically. Change the style definition and every paragraph that references it updates along with it, which is exactly what setting the formatting on a paragraph directly cannot do. Spire.Doc for JavaScript performs all of this in the browser via WebAssembly, using a virtual file system (VFS) to manage input and output files — no backend server required.
Structurally a paragraph style holds two parts at once: character formatting and paragraph formatting. Once a built-in style has been taken, ParagraphStyle.CharacterFormat changes the text-level properties and ParagraphStyle.ParagraphFormat changes the paragraph-level ones. The library maps this model onto the Document.Styles collection in full.
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.
Modify the Built-in Styles
Word ships with a batch of built-in styles — Title, Normal, Heading 1 to Heading 9 and so on. document.AddStyle({ builtinStyle }) takes the named built-in style (creating it if it does not exist yet) and returns its object, and once you have it, it can be rewritten.
Note that the return type of AddStyle is the generic Style, so before changing the paragraph format you have to confirm that the object really is a paragraph style — instanceof wasmModule.ParagraphStyle is the check. Normal is the base style of the body text, and changing it also affects every style that inherits from it, so it is usually used only to unify the body font and the font size.
function App() {
const Styles = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
await window.spire.FetchFileToVFS("ARIALUNI.TTF", "/Library/Fonts/", `${process.env.PUBLIC_URL}static/font/`);
// Create the document and the section
let doc = new docModule.Document();
let sec = doc.AddSection();
// Take the built-in Title style and rewrite it with a custom colour scheme:
// a bottom border and left alignment
let titleStyle = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Title });
// Check whether it is a paragraph style; if it is, set the paragraph format too
if (titleStyle instanceof docModule.ParagraphStyle) {
let ps = titleStyle;
ps.CharacterFormat.FontName = "Arial Unicode MS";
ps.CharacterFormat.FontSize = 28;
ps.CharacterFormat.TextColor = docModule.Color.FromArgb(42, 123, 136);
ps.ParagraphFormat.Borders.Bottom.BorderType = docModule.BorderStyle.Single;
ps.ParagraphFormat.Borders.Bottom.Color = docModule.Color.FromArgb(42, 123, 136);
ps.ParagraphFormat.Borders.Bottom.LineWidth = 1.5;
ps.ParagraphFormat.HorizontalAlignment = docModule.HorizontalAlignment.Left;
}
// Body style: one font and one size for the body text
let normalStyle = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Normal });
normalStyle.CharacterFormat.FontName = "Arial Unicode MS";
normalStyle.CharacterFormat.FontSize = 11;
// Heading 1 style
let heading1Style = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
heading1Style.CharacterFormat.FontName = "Arial Unicode MS";
heading1Style.CharacterFormat.FontSize = 14;
heading1Style.CharacterFormat.Bold = true;
heading1Style.CharacterFormat.TextColor = docModule.Color.FromArgb(42, 123, 136);
// Heading 2 style
let heading2Style = doc.AddStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
heading2Style.CharacterFormat.FontName = "Arial Unicode MS";
heading2Style.CharacterFormat.FontSize = 12;
heading2Style.CharacterFormat.Bold = true;
// Custom bulleted list style
let bulletList = doc.Styles.Add({ listType: docModule.ListType.Bulleted, name: "bulletList" });
doc.Styles.Add({ style: bulletList });
// Apply the styles: built-in styles by builtinStyle, custom styles by name
let paragraph = sec.AddParagraph();
paragraph.AppendText("Quarterly Operations Report");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Title });
paragraph = sec.AddParagraph();
paragraph.AppendText("Prepared by: Operations Management Department | Date: September 2026");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Normal });
paragraph = sec.AddParagraph();
paragraph.AppendText("Overall Progress");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
paragraph = sec.AddParagraph();
paragraph.AppendText("All three product lines stayed on plan this quarter, and the overall delivery pace was steady.");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Normal });
paragraph = sec.AddParagraph();
paragraph.AppendText("Key Items");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });
paragraph = sec.AddParagraph();
paragraph.AppendText("Key Milestones");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
paragraph = sec.AddParagraph();
paragraph.AppendText("Core module integration testing completed");
paragraph.ListFormat.ApplyStyle("bulletList");
paragraph = sec.AddParagraph();
paragraph.AppendText("Trial run phase started");
paragraph.ListFormat.ApplyStyle("bulletList");
paragraph = sec.AddParagraph();
paragraph.AppendText("Pre-release review scheduled");
paragraph.ListFormat.ApplyStyle("bulletList");
paragraph = sec.AddParagraph();
paragraph.AppendText("Resource Input");
paragraph.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading2 });
paragraph = sec.AddParagraph();
paragraph.AppendText("Team capacity is tight at the moment; confirm the schedule early in the quarter.");
paragraph.ListFormat.ApplyStyle("bulletList");
// Define the output file name
const outputFileName = "Styles-result.docx";
// Save the document to the given 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>Create Styles and Apply Them to Paragraphs</h1>
<button onClick={Styles}>Generate</button>
</div>
);
}
export default App;
The built-in Title, Heading 1 and Heading 2 have been rewritten into one teal colour scheme, and the custom bulletList supplies the bullets

Copy Styles Between Documents
Companies often keep a "style master" document whose styles new documents have to follow. Rebuilding those styles one at a time by hand is slow and easy to miss something, so walk the Styles collection of the source document and add every style object to the target document instead.
document.Styles supports Count and get_Item(index), so the collection can be walked completely by index. Once the styles of the source document have been added to the target document, the paragraphs in the target that reference those style names immediately show the formatting of the source document.
This example uses two sample documents: CopyDocumentStyles1.docx is the source document with custom styles, and CopyDocumentStyles2.docx is the target document — some of its paragraphs reference style names that only the source document defines, but it does not define them itself, so before the copy those paragraphs are displayed with the default formatting.
function App() {
const CopyDocumentStyles = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the two sample files into the virtual file system (VFS)
let inputFileName_1 = "CopyDocumentStyles1.docx";
await window.spire.FetchFileToVFS(inputFileName_1, "", `${process.env.PUBLIC_URL}static/data/`);
let inputFileName_2 = "CopyDocumentStyles2.docx";
await window.spire.FetchFileToVFS(inputFileName_2, "", `${process.env.PUBLIC_URL}static/data/`);
// Load the source document (with the custom styles)
let srcDoc = new docModule.Document();
srcDoc.LoadFromFile(inputFileName_1);
// Load the target document (built-in styles only)
let destDoc = new docModule.Document();
destDoc.LoadFromFile(inputFileName_2);
// Take the style collection of the source document
let styles = srcDoc.Styles;
// Add them to the target document one by one
for (let i = 0; i < styles.Count; i++) {
let style = styles.get_Item(i);
destDoc.Styles.Add(style);
}
// Define the output file name
const outputFileName = "CopyDocumentStyles_result.docx";
// Save the document to the given path
destDoc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });
destDoc.Dispose();
srcDoc.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>Copy the Styles of the Source Document into the Target Document</h1>
<button onClick={CopyDocumentStyles}>Generate</button>
</div>
);
}
export default App;
After the copy the target document has the custom styles of the source document, and the paragraphs that "referenced a style without carrying its formatting" are displayed correctly again

FAQ
The Normal style was changed, but not every body paragraph followed
Cause: Only styles that inherit from Normal are affected by it. If a paragraph carries direct formatting of its own (a per-paragraph CharacterFormat.FontName, for example), the direct formatting has a higher priority than the style and covers up what the style sets.
Solution: Control the appearance through styles consistently and remove the direct formatting from the paragraphs and the runs. Normal is commonly used to unify the body font and the font size:
let normalStyle = document.AddStyle({ builtinStyle: wasmModule.BuiltinStyle.Normal });
normalStyle.CharacterFormat.FontName = "Arial Unicode MS";
normalStyle.CharacterFormat.FontSize = 11;
The number of styles in the target document doubled after the copy
Cause: The walk copies every style of the source document across, and that includes a large number of built-in styles. Those style names usually already exist in the target document, and adding them one at a time creates duplicate entries.
Solution: This is the actual behaviour of the sample (the style collection grows noticeably). If a real project only needs the custom styles, filter by name first and copy only the styles the target document does not have yet:
for (let i = 0; i < srcDoc.Styles.Count; i++) {
let style = srcDoc.Styles.get_Item(i);
// Check whether the target document already has a style with the same name
let exists = false;
for (let j = 0; j < destDoc.Styles.Count; j++) {
if (destDoc.Styles.get_Item(j).Name === style.Name) {
exists = true;
break;
}
}
// Only add the styles that the target document is missing
if (!exists) {
destDoc.Styles.Add(style);
}
}
A custom style does not take effect when it is applied to a paragraph
Cause: ApplyStyle is called differently for built-in and custom styles — a built-in style takes a { builtinStyle } object, a custom style takes the style name as a string. Passing the wrong form is ignored silently.
Solution: Choose the form that matches where the style comes from:
// Built-in style
paragraph.ApplyStyle({ builtinStyle: wasmModule.BuiltinStyle.Heading1 });
// Custom style (the string name, which has to match the name used in Add)
paragraph.ListFormat.ApplyStyle("bulletList");
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 between Word documents and OpenXML is one of the most frequent needs in document processing: exporting a Word document to XML lifts the body content out of the binary package structure, which makes cross-system exchange, bulk extraction and archiving much easier, while turning XML back into a Word document lets you generate a standard document directly from structured data you already have. Spire.Doc for JavaScript performs the conversion in both directions in the browser via WebAssembly, using a virtual file system (VFS) to manage fonts and files — no backend server required. OpenXML flat files come in two formats: WordML for Word 2003 and WordXml (Flat OPC) for Word 2007 and later, so the one you save as depends on the target Word version.
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.
Word to OpenXML (WordML)
Converting a Word document to OpenXML involves three steps: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the file and call SaveToFile with FileFormat.WordML to save the document as a Word 2003 XML flat file; finally, read the generated XML file from VFS, wrap it as a Blob and trigger a browser download.
function App() {
const WordToWordML = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample file into VFS
let inputFileName = 'WordToWordXML.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// Create a document instance
const doc = new docModule.Document();
// Load the document from the virtual file system
doc.LoadFromFile(inputFileName);
// Define the output file name and save it as a Word 2003 OpenXML file
const outputFileName = 'WordToWordXML-result.xml';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.WordML });
// 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>Convert Word To WordML</h1>
<button onClick={WordToWordML}>Generate</button>
</div>
);
}
export default App;
With FileFormat.WordML passed to SaveToFile, the Word document is converted into a Word 2003 XML flat file

Word to OpenXML (WordXml)
The flat file format of Word 2007 and later is called WordXml (Flat OPC): it packs the individual parts of the docx package — body, styles, relationships and so on — into a single XML file whose root element is <pkg:package>, and Word can open it directly just the same. The conversion flow is identical to WordML; only the save format differs. It again involves three steps: first, load the font files and the target Word document into the WASM virtual file system via FetchFileToVFS; then instantiate a Document, load the file and call SaveToFile with FileFormat.WordXml to save the document as a Word 2007 XML file; finally, read the generated XML file from VFS, wrap it as a Blob and trigger a browser download.
function App() {
const WordToWordXml = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample file into VFS
let inputFileName = 'WordToWordXML.docx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// Create a document instance
const doc = new docModule.Document();
// Load the document from the virtual file system
doc.LoadFromFile(inputFileName);
// Define the output file name and save it as a Word 2007 OpenXML file
const outputFileName = 'WordToWordXML-result-2007.xml';
doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.WordXml });
// 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>Convert Word To WordXml</h1>
<button onClick={WordToWordXml}>Generate</button>
</div>
);
}
export default App;
With FileFormat.WordXml passed to SaveToFile, the parts of the Word document are merged into a single Word 2007 Flat OPC XML file

OpenXML to Word
Converting OpenXML to Word involves three steps: first, load the font files and the target XML file into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the XML file directly with LoadFromFile — Spire.Doc recognizes the WordML and WordXml formats from the file contents automatically — and call SaveToFile to save it as a Docx2013 Word document; finally, read the generated .docx file from VFS, wrap it as a Blob and trigger a browser download.
function App() {
const XMLToWord = async () => {
const docModule = window.wasmModule?.spiredoc;
if (!docModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample file into VFS
let inputFileName = 'XMLToWord.xml';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);
// Create a document instance
const doc = new docModule.Document();
// Load the OpenXML file from the virtual file system
doc.LoadFromFile(inputFileName);
// Define the output file name and save it as a Word document
const outputFileName = 'XMLToWord-result.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>Convert XML To Word</h1>
<button onClick={XMLToWord}>Generate</button>
</div>
);
}
export default App;
After the OpenXML file is loaded with LoadFromFile and saved as a Word document, the paragraphs, lists and tables it contains are restored in full

FAQ
The generated XML reports a format error when opened in Word
Cause: The save format does not match the target Word version. FileFormat.WordML maps to the Word 2003 XML format and FileFormat.WordXml to the Word 2007 and later Flat OPC format; the two have different structures, so opening a file saved in the newer format with an older version of Word fails.
Solution: Pick the enum value that matches the target Word version, and name the output file .xml:
// Word 2003 and earlier
doc.SaveToFile({ fileName: 'out.xml', fileFormat: wasmModule.FileFormat.WordML });
// Word 2007 and later
doc.SaveToFile({ fileName: 'out.xml', fileFormat: wasmModule.FileFormat.WordXml });
Loading the XML fails, or the converted document comes out empty
Cause: OpenXML to Word requires the input to be a complete WordprocessingML flat file; an ordinary custom data XML cannot be recognized by LoadFromFile.
Solution: Make sure the input XML was exported by Word or Spire.Doc — the root element of the WordML format is <w:wordDocument> and that of the WordXml format is <pkg:package>:
const doc = new wasmModule.Document();
doc.LoadFromFile('XMLToWord.xml');
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.