Add a Table of Contents to an Existing Word Document with JavaScript in React

A more common situation in real-world work is a document that is already written and has a complete chapter structure, but no table of contents was generated at the time. There is no need to rearrange the content — adding a TOC field on top of the existing heading styles is enough to produce a complete table of contents with page numbers and hyperlinks. Spire.Doc for JavaScript opens and edits Word documents directly in the browser via WebAssembly, managing input and output files through a virtual file system (VFS) — no backend server required.

Compared with creating a new document, adding a table of contents to an existing document involves two extra key steps: loading the original document from VFS with LoadFromFile, and moving the table of contents paragraph to the very beginning of the document with Paragraphs.Insert, instead of appending it to the end by default.

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.


Add a Default Table of Contents

Adding a default table of contents to an existing document has three phases: first, load the font file and the Word document to be processed into the WASM virtual file system via FetchFileToVFS; then instantiate a Document and load the document with LoadFromFile, create a new paragraph and insert the TOC field with AppendTOC, and move it to the very beginning of the document with Paragraphs.Insert(0, tocPara); finally, call UpdateTableOfContents to fill in the entries and page numbers, save the document, read the generated file from VFS, wrap it as a Blob, and trigger a browser download.

The input document used in the example, AddTocToExisting.docx, is a technical report with three chapters and eleven multi-level headings but no table of contents yet.

function App() {
  const AddTableOfContentsToExistingDocument = 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 font and the existing Word document into VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'AddTocToExisting.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // Create a document instance and load the existing document
    const doc = new docModule.Document();
    doc.LoadFromFile({ fileName: inputFileName });

    // Get the first section of the document
    let section = doc.Sections.get_Item(0);

    // Create a new paragraph and insert the TOC field, collecting Heading 1 through Heading 3 entries
    let tocPara = section.AddParagraph();
    tocPara.AppendTOC(1, 3);

    // Move the table of contents paragraph to the very beginning of the document
    section.Paragraphs.Insert(0, tocPara);

    // Update the table of contents to fill in entries and page numbers
    doc.UpdateTableOfContents();

    // Define the output file name and save
    const outputFileName = "Add a Default TOC to an Existing Document.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // Release resources
    doc.Dispose();

    // Read the generated file from VFS and trigger the download
    const 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>Click the button below to add a default table of contents to an existing document</h1>
      <button onClick={AddTableOfContentsToExistingDocument}>
        Generate
      </button>
    </div>
  );
}

export default App;

After the existing document is loaded with LoadFromFile and a TOC field is inserted, the table of contents is placed at the very beginning, while the original chapter content and layout remain unchanged

After the existing document is loaded with LoadFromFile and a TOC field is inserted, the table of contents is placed at the very beginning, while the original chapter content and layout remain unchanged


Add a Custom Table of Contents

The table of contents generated by AppendTOC uses Word's default field switches. When you need to control its exact behavior, you can construct a TableOfContent object directly and specify the switch string instead. The difference from the previous feature lies in how it is inserted: you must manually add the table of contents object to a paragraph, supply the field separator and field end marks, and assign the object to document.TOC. The commonly used field switches and their meanings are as follows:

Switch Description
\o "1-3" Collects entries by built-in heading styles; here it means including Heading 1 through Heading 3
\h Turns table of contents entries into hyperlinks that jump to the corresponding chapter when clicked
\z Hides page numbers and tab leaders in Web Layout view
\u Collects entries by the outline level of the paragraphs

If you want the table of contents to occupy its own page and be separated from the body, add a page break to the same section after inserting the table of contents paragraph:

tocPara.AppendBreak(docModule.BreakType.PageBreak);
function App() {
  const CustomizeTableOfContent = 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 font and the existing Word document into VFS
    await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    const inputFileName = 'AddTocToExisting.docx';
    await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);

    // Create a document instance and load the existing document
    const doc = new docModule.Document();
    doc.LoadFromFile({ fileName: inputFileName });

    // Get the first section of the document
    let section = doc.Sections.get_Item(0);

    // Construct a table of contents object with custom field switches
    let toc = new docModule.TableOfContent(doc, "{\\o \"1-3\" \\h \\z \\u}");

    // Add the table of contents object to a paragraph
    let tocPara = section.AddParagraph();
    tocPara.Items.Add(toc);

    // Supply the field separator and field end marks
    tocPara.AppendFieldMark(docModule.FieldMarkType.FieldSeparator);
    tocPara.AppendText("TOC");
    tocPara.AppendFieldMark(docModule.FieldMarkType.FieldEnd);

    // Bind this table of contents to the document
    doc.TOC = toc;

    // Move the table of contents paragraph to the very beginning of the document
    section.Paragraphs.Insert(0, tocPara);

    // Update the table of contents to fill in entries and page numbers
    doc.UpdateTableOfContents();

    // Define the output file name and save
    const outputFileName = "Add a Custom TOC to an Existing Document.docx";
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Docx2013 });

    // Release resources
    doc.Dispose();

    // Read the generated file from VFS and trigger the download
    const 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>Click the button below to add a custom table of contents to an existing document</h1>
      <button onClick={CustomizeTableOfContent}>
        Generate
      </button>
    </div>
  );
}

export default App;

The table of contents generated with a TableOfContent object and custom field switches has its entry levels, hyperlinks, and page numbers all determined by the switch string

The table of contents generated with a TableOfContent object and custom field switches has its entry levels, hyperlinks, and page numbers all determined by the switch string


FAQ

The table of contents appears at the end of the document instead of the beginning

Cause: AddParagraph appends a new paragraph to the end of its section by default, so inserting the TOC field directly on it naturally places the table of contents at the end as well. The content of an existing document has already been laid out, so the insertion position must be specified explicitly.

Solution: Create the table of contents paragraph first, then move it to the very beginning of the document with Paragraphs.Insert:

let tocPara = section.AddParagraph();
tocPara.AppendTOC(1, 3);
section.Paragraphs.Insert(0, tocPara);

The table of contents is empty

Cause: A TOC field collects entries by heading style. If the chapter headings in the original document were only manually bolded and enlarged without built-in heading styles such as Heading1 to Heading3 applied, no entries will appear in the table of contents after updating.

Solution: First check whether the headings in the original document use built-in heading styles. If not, re-apply the style to those paragraphs after loading the document:

let heading = section.Paragraphs.get_Item(2);
heading.ApplyStyle({ builtinStyle: docModule.BuiltinStyle.Heading1 });

Page numbers in the table of contents are missing or incorrect

Cause: AppendTOC only inserts the TOC field itself; the field content must be updated explicitly. If UpdateTableOfContents is not called before saving, the generated table of contents contains only the field code, with no entries or page numbers.

Solution: Call the update method before SaveToFile:

doc.UpdateTableOfContents();
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.