Knowledgebase (2597)
Children categories
Documents are often assembled from pieces: the cover, the body and the appendix arrive from different stages, and only before delivery does it turn out that the cover ended up after Chapter 2, or that a few pages need to change places. Only the page order has to change — not a word of the content — but without a PDF editor at hand the job stalls right there, and sending the file to a server means it leaves the user's device. This article shows how to rearrange PDF pages in a specified order in the browser with Spire.PDF for JavaScript. It loads, modifies and saves PDF documents based on WebAssembly, so the page order is rewritten entirely on the local machine, reading and writing files through a virtual file system (VFS), with no backend service required.
For installation and project configuration, refer to Integrating Spire.PDF for JavaScript in a React Project. The following examples assume Spire.PDF is installed and the WebAssembly module has been initialized.
Rearrange the Page Order
The page order is rewritten in a single call to PdfPageCollection.ReArrange(). The array of indices decides both where each page goes and how many pages the document has — as many indices as you pass, as many pages you get — so to keep every page, all indices from 0 to the page count minus one have to appear.
function App() {
const rearrangePages = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file to be processed into the VFS
const inputFileName = 'Number.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// The new page order: move the third page to the front and shift the rest down; indices start at 0
const newOrder = [2, 0, 1, 3, 4];
doc.Pages.ReArrange(newOrder);
const outputFileName = 'Rearranged_Pages.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
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>Rearrange PDF Pages</h1>
<button onClick={rearrangePages}>
Start Rearranging
</button>
</div>
);
}
export default App;
The document after the third page is moved to the front and the remaining pages shift down:

FAQ
The document has fewer pages after rearranging
Cause: ReArrange() rebuilds the page order from the array you pass, so a page whose index is missing from that array never makes it into the result document. Call ReArrange([1, 0]) on a five-page document and the output has two pages.
Solution: To keep the page count unchanged, the array length must equal doc.Pages.Count, with every index from 0 to doc.Pages.Count - 1 appearing exactly once:
// Indices start at 0: a five-page document uses 0, 1, 2, 3 and 4
const pageCount = doc.Pages.Count;
const fullOrder = Array.from({ length: pageCount }, (_, i) => i);
doc.Pages.ReArrange(fullOrder);
Calling ReArrange() reports "The page has existed."
Cause: The array contains a duplicate index, so the same original page is assigned to two positions. One page cannot occupy two positions at the same time, and Spire.PDF raises an error.
Solution: Make sure the array is a permutation of the original page indices — each index appears once and stays within range. For a five-page document the valid indices are 0 to 4.
How to swap only two pages
Cause: ReArrange() always works on the order of the whole document, and there is no shorter overload that swaps two pages on its own.
Solution: Still write the full order array, leaving the untouched positions at their original indices. The line below swaps the first and the second page:
// Swap the first two pages: 0 and 1 trade places, the rest stay put
const swappedOrder = [1, 0, 2, 3, 4];
doc.Pages.ReArrange(swappedOrder);
Get a Free License
If you wish to remove the evaluation message from the result document or remove feature limitations, please contact sales to obtain a temporary license valid for 30 days.
Set PDF Background Color or Background Image in React Using JavaScript
2026-09-18 06:08:07 Written by Nina TangWhen a batch of documents has to look like one set, the quickest way is to give every page the same brand tint or letterhead pattern. Doing that used to mean going back to the source files and reworking the layout one by one, or stacking an image on each page by hand — the first requires that you still have the editable originals, and the second easily ends up covering the body text.
Editing the PDF directly in the browser sidesteps both. Spire.PDF for JavaScript loads, modifies and saves PDF documents on WebAssembly; the tint and the background image are written into the BackgroundColor and BackgroundImage properties of pages that already exist, drawn below the body text; files are read and written through a virtual file system (VFS), with no backend involved.
This article covers two core features:
For installation and project configuration, refer to Integrating Spire.PDF for JavaScript in a React Project. The following examples assume Spire.PDF is installed and the WebAssembly module has been initialized.
Set a Background Color on All PDF Pages
When the whole document needs one tint, assigning a Color to BackgroundColor page by page is enough. The property blends at an opacity of 0.25 by default, so set BackgroudOpacity to 1 when you want the solid color.
function App() {
const setBackgroundColor = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file to be processed into the VFS
const inputFileName = 'Lease_Agreement_EN.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Custom tint: four ARGB channels, light blue here
const backgroundColor = pdfModule.Color.FromArgb(255, 226, 240, 253);
// Set the background color page by page
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
page.BackgroundColor = backgroundColor;
// The default blend opacity is 0.25; set it to 1 for the solid color
page.BackgroudOpacity = 1;
}
const outputFileName = 'SetBackgroundColor.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file back from the VFS to trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
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 PDF Background Color</h1>
<button onClick={setBackgroundColor}>
Start Setting
</button>
</div>
);
}
export default App;
With the light blue background applied, the lease agreement takes on the same tint throughout:

Set a Background Image on All PDF Pages
To lay a full-page image underneath, hand it to BackgroundImage: it takes an image stream opened in the virtual file system and stretches it to fill the page content area, with BackgroudOpacity controlling how strongly it shows.
function App() {
const setBackgroundImage = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file to be processed and the background image into the VFS
const inputFileName = 'Lease_Agreement_EN.pdf';
const imageFileName = 'Background.png';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Open the background image as a file stream in the VFS; every page shares one stream
const imageStream = new window.spire.Stream(imageFileName);
// Lay the image down page by page; it stretches to fill the page content area
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
page.BackgroundImage = imageStream;
page.BackgroudOpacity = 1;
}
const outputFileName = 'SetBackgroundImage.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file back from the VFS to trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
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 PDF Background Image</h1>
<button onClick={setBackgroundImage}>
Start Setting
</button>
</div>
);
}
export default App;
The same background image fills every page, and the body text stays legible:

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

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

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

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

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