JavaScript (260)
Create a Custom (Combination) Chart in React with JavaScript
2026-10-10 01:21:27 Written by liu taliaDrawing a single chart type per figure is Excel's default behaviour, not a rule anyone has to keep. A report usually has more than one thing to say, and each reading has a shape that suits it: height suits columns, movement suits a line, a running total suits an area. A custom (combination) chart exists for exactly this: within one chart, each series can be given a chart type of its own, and which types are put together is decided by the questions the figure has to answer side by side -- there is no fixed pairing. Columns with a line is the pairing most widely used in practice, because it accommodates both readings at once. The example below takes that pair, drawing sales as columns and the target as a line, so that what the reader compares is the line's position against the column tops. Two separate charts would need their category axes aligned first, before any comparison could be made; one chart, with a category axis the series share, keeps the months strictly aligned. A report also often holds a series that cannot be brought into the main data range, and such a series has to be added by hand. Spire.XLS for JavaScript performs all of this in the browser on top of WebAssembly, managing input and output files through a virtual file system (VFS), with no backend service required.
This article covers two key features:
For installation and project setup, see Integrating Spire.XLS for JavaScript in a React Project. The examples below assume Spire.XLS is installed and the WebAssembly module has been initialised.
Combine a column chart with a line chart
A pairing is not fixed: which types share one figure depends on which questions have to be answered side by side, and columns with a line is only the most widely used of them. In this example sales are best expressed as columns, where heights are compared at a glance, and the target is best expressed as a line, where what the reader compares is its position against the column tops. Once the two series have a type each, one figure carries both readings, and the category axis they share keeps the months strictly aligned, so no comparison across two figures is needed. Substituting clustered bars with a line, or columns with an area, works the same way. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFileand take the first worksheet. - Add a chart with
sheet.Charts.Add()and pointchart.DataRangeatA1:C7: the first row supplies the series names, the first column the category labels, and the next two columns become one series each. - Set
chart.SeriesDataFromRangetofalseso that each series keeps the values bound to it. - Place the chart on the sheet with
chart.LeftColumn,chart.TopRow,chart.RightColumnandchart.BottomRow. - Give
chart.Series.get(0).SerieTypeandchart.Series.get(1).SerieTypethe valuesExcelChartType.ColumnClusteredandExcelChartType.Line. Every series names its own type this way, so which types go together is yours to choose. - Set the title with
chart.ChartTitle, then save the workbook withworkbook.SaveToFile.
The complete code example below shows how to combine a column chart with a line chart in React:
function App() {
const combineColumnAndLine = async () => {
// get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// check that the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesReport.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// take the first worksheet
const sheet = workbook.Worksheets.get(0);
// add a chart and bind its data range to A1:C7: the first column becomes the category labels, the next two become one series each
const chart = sheet.Charts.Add();
chart.DataRange = sheet.Range.get('A1:C7');
chart.SeriesDataFromRange = false;
// place the chart
chart.LeftColumn = 1;
chart.TopRow = 10;
chart.RightColumn = 9;
chart.BottomRow = 26;
// give the two series a chart type each: sales drawn as columns, target sales drawn as a line
chart.Series.get(0).SerieType = xlsModule.ExcelChartType.ColumnClustered;
chart.Series.get(1).SerieType = xlsModule.ExcelChartType.Line;
chart.ChartTitle = 'Sales and Target Sales';
// save the workbook
const outputFileName = 'CombineColumnAndLine.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// dispose of the workbook to free resources
workbook.Dispose();
// read the result file from the VFS and start the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Custom (Combination) Chart</h1>
<button id="combine-column-and-line" onClick={combineColumnAndLine}>Combine a column chart with a line chart</button>
</div>
);
}
export default App;
After running, the effect of combining a column chart with a line chart:
Add a series by hand
DataRange can only ever be one continuous rectangle, and that is a hard limit on how data gets bound. Reports commonly present two such cases: the main data occupies columns A to C while the reference values start a column further along, with an empty column in between; or the values to be plotted are derived by calculation and occupy no cell at all. Neither case can be bound in a single call, so the chart has to be built series by series instead. A hand-added series does not depend on DataRange: its range can point anywhere, and it still takes a chart type of its own, so the rules of a combination chart apply to it in full. Here last year's sales are added as a second column series, paired alongside the current year, with the target line running across both. The steps are:
- Load the font and the test data file into the VFS, load the workbook and take the first worksheet.
- Add a chart with
sheet.Charts.Add()and pointchart.DataRangeat the main block alone,A1:C7. - Give the two series of that block
ExcelChartType.ColumnClusteredandExcelChartType.Line. - Add a series with
chart.Series.Add({ name, serieType }), which takes the series name and its chart type in one call. - Bind the category labels and the values with
series.CategoryLabelsandseries.Values, each pointing at the range the data actually occupies. - Set the title with
chart.ChartTitle, then save the workbook withworkbook.SaveToFile.
The complete code example below shows how to add a series by hand in React:
function App() {
const addSeriesManually = async () => {
// get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// check that the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'SalesReport.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// take the first worksheet
const sheet = workbook.Worksheets.get(0);
// add a chart and bind its data range to the main block, A1:C7
const chart = sheet.Charts.Add();
chart.DataRange = sheet.Range.get('A1:C7');
chart.SeriesDataFromRange = false;
// place the chart
chart.LeftColumn = 1;
chart.TopRow = 10;
chart.RightColumn = 9;
chart.BottomRow = 26;
// the two series of the main block: sales as columns, target sales as a line
chart.Series.get(0).SerieType = xlsModule.ExcelChartType.ColumnClustered;
chart.Series.get(1).SerieType = xlsModule.ExcelChartType.Line;
// last year's figures sit in column E, one empty column away from the main block, so they cannot come along with the data range; add a series by hand instead.
// a hand-added series takes a chart type of its own too; here it is drawn as columns alongside sales
const lastYear = chart.Series.Add({ name: 'Last Year Sales (K)', serieType: xlsModule.ExcelChartType.ColumnClustered });
lastYear.CategoryLabels = sheet.Range.get('A2:A7');
lastYear.Values = sheet.Range.get('E2:E7');
chart.ChartTitle = 'Sales, Target Sales and Last Year Sales';
// save the workbook
const outputFileName = 'AddSeriesManually.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// dispose of the workbook to free resources
workbook.Dispose();
// read the result file from the VFS and start the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Custom (Combination) Chart</h1>
<button id="add-series-manually" onClick={addSeriesManually}>Add a series by hand</button>
</div>
);
}
export default App;
After running, the effect of adding a series by hand:
FAQ
Can a hand-added series be used alongside DataRange?
Cause: Yes, and the two do not interfere. DataRange produces only the series that correspond to the columns of the main block; a hand-added series carries its own Values and CategoryLabels, which are unrelated to the range DataRange covers and need not lie within the same rectangle. In the second example above the chart holds both series of the main block and the hand-added last-year series at once.
Solution: Point DataRange at the main block, then add each extra series with chart.Series.Add({ name, serieType }). Note that a hand-added series does not inherit the category labels of the main block: CategoryLabels has to be assigned explicitly, and if it is pointed at the same column the series share one category axis.
// the two series of the main block come from DataRange; the hand-added one is bound separately
chart.DataRange = sheet.Range.get('A1:C7');
const lastYear = chart.Series.Add({ name: 'Last Year Sales (K)', serieType: xlsModule.ExcelChartType.ColumnClustered });
lastYear.CategoryLabels = sheet.Range.get('A2:A7');
lastYear.Values = sheet.Range.get('E2:E7');
What type does a series added without serieType get?
Cause: It takes the chart's own type. sheet.Charts.Add() is called with no arguments in the example, and the chart it builds defaults to ExcelChartType.ColumnClustered, which is the type a new series keeps as well.
Solution: For a new series to join a combination chart, name its type at the call, or change SerieType after adding it:
// named at the call
const serie = chart.Series.Add({ name: 'Last Year Sales (K)', serieType: xlsModule.ExcelChartType.ColumnClustered });
// or changed after adding
serie.SerieType = xlsModule.ExcelChartType.Line;
Get a Free License
Spire.XLS for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.
Once a contract or approval form is finalized, it often needs an "Approved" stamp or a timestamp in the corner to record who confirmed it and when. Doing this in desktop software means stamping each file by hand, which gets slow as the pile grows; sending the document to a server to process means the file leaves the local machine.
Spire.PDF for JavaScript loads, modifies, and saves PDF documents in the browser with WebAssembly. Stamps are generated locally and files are read and written through a virtual file system (VFS), with no backend involved. The library treats a stamp as a type of annotation: PdfRubberStampAnnotation sets the stamp's position and size on the page, while its appearance comes from a PdfTemplate assigned through PdfAppearance — draw an image on the template for an image stamp, or text for a text stamp.
This article covers three core features:
For installation and project setup, see How to Integrate Spire.PDF for JavaScript in a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.
Add an Image Stamp
Spire.PDF for JavaScript provides the PdfRubberStampAnnotation class for placing a stamp at a given position on the page. It only defines the stamp's position and size; the appearance is set through PdfAppearance.
function App() {
const addImageStamp = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check that the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF and the stamp image into the VFS
const inputFileName = 'SampleDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
const stampImageName = 'Seal.png';
await window.spire.FetchFileToVFS(stampImageName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Get the first page
let page = doc.Pages.get_Item(0);
// Load the stamp image
let image = pdfModule.PdfImage.FromFile(stampImageName);
// Scale the image to fit within 120 points
const maxSize = 120;
const scale = Math.min(maxSize / image.Width, maxSize / image.Height, 1);
const stampWidth = image.Width * scale;
const stampHeight = image.Height * scale;
// Create a template the same size as the stamp and fill it with the image
let template = new pdfModule.PdfTemplate({ width: stampWidth, height: stampHeight });
template.Graphics.DrawImage({ image: image, x: 0, y: 0, width: stampWidth, height: stampHeight });
// Frame the stamp in the bottom-right corner; the rectangle matches the template
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40, page.ActualSize.Height - stampHeight - 40),
size: new pdfModule.SizeF({ width: stampWidth, height: stampHeight })
});
// Create the stamp annotation and set the template as its appearance
let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
let appearance = new pdfModule.PdfAppearance(stamp);
appearance.Normal = template;
stamp.Appearance = appearance;
// Add the stamp annotation to the page's annotation collection
page.AnnotationsWidget.Add(stamp);
// Save the document
const outputFileName = 'ImageStamp.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>Add an Image Stamp</h1>
<button onClick={addImageStamp}>
Add Stamp
</button>
</div>
);
}
export default App;
A PDF document with an image stamp in the bottom-right corner:

Add a Text Stamp
Spire.PDF for JavaScript also provides PdfPath, PdfPen, and PdfTrueTypeFont for drawing text as a stamp. PdfPath builds a rounded rectangle from four arcs, DrawPath strokes it into a border, and DrawString writes the text inside — a text stamp with a border.
function App() {
const addTextStamp = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check that the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load a font into the VFS font directory
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the PDF into the VFS
const inputFileName = 'SampleDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Get the first page
let page = doc.Pages.get_Item(0);
// Create the template that holds the stamp
let template = new pdfModule.PdfTemplate({ width: 125, height: 55 });
// Build the font from the file in the VFS and prepare the brush
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12, style: pdfModule.PdfFontStyle.Bold });
let brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_DarkRed() }) });
let pen = new pdfModule.PdfPen({ brush: brush });
// Build a rounded-rectangle path from four arcs and a line
const radius = 20;
let path = new pdfModule.PdfPath();
path.AddArc({ x: 0, y: 0, width: radius, height: radius, startAngle: 180, sweepAngle: 90 });
path.AddArc({ x: template.Width - radius, y: 0, width: radius, height: radius, startAngle: 270, sweepAngle: 90 });
path.AddArc({ x: template.Width - radius, y: template.Height - radius, width: radius, height: radius, startAngle: 0, sweepAngle: 90 });
path.AddArc({ x: 0, y: template.Height - radius, width: radius, height: radius, startAngle: 90, sweepAngle: 90 });
path.AddLine({ x1: 0, y1: template.Height - radius, x2: 0, y2: radius / 2 });
// Stroke the path into the stamp border
template.Graphics.DrawPath({ pen: pen, path: path });
// Write two lines of text inside the border
template.Graphics.DrawString({ s: 'APPROVED', font: font, brush: brush, point: new pdfModule.PointF(28, 10) });
template.Graphics.DrawString({ s: 'Marketing Dept.', font: font, brush: brush, point: new pdfModule.PointF(10, 32) });
// Frame the stamp in the bottom-right corner; the rectangle matches the template
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - template.Width - 40, page.ActualSize.Height - template.Height - 40),
size: template.Size
});
// Create the stamp annotation and set the template as its appearance
let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
let appearance = new pdfModule.PdfAppearance(stamp);
appearance.Normal = template;
stamp.Appearance = appearance;
// Add the stamp annotation to the page's annotation collection
page.AnnotationsWidget.Add(stamp);
// Save the document
const outputFileName = 'TextStamp.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>Add a Text Stamp</h1>
<button onClick={addTextStamp}>
Add Stamp
</button>
</div>
);
}
export default App;
A text stamp with a rounded border and the word APPROVED:

Add a Date and Time Stamp
Spire.PDF for JavaScript also provides PdfTemplate for rendering arbitrary content as a stamp appearance. The code below draws a single line of the current time on the template: the string is generated with the browser's native Date, and since DrawString draws any plain string, the stamp content changes with the moment it is generated.
function App() {
const addDateTimeStamp = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check that the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load a font into the VFS font directory
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the PDF into the VFS
const inputFileName = 'SampleDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Get the first page
let page = doc.Pages.get_Item(0);
// Get the current date and time
const now = new Date();
const pad = (value) => String(value).padStart(2, '0');
let timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
// Create a template sized for the text
let template = new pdfModule.PdfTemplate({ width: 150, height: 16 });
// Prepare the font and brush
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12 });
let brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }) });
// Draw the time text onto the template
template.Graphics.DrawString({ s: timeString, font: font, brush: brush, point: new pdfModule.PointF(0, 0) });
// Frame the stamp in the bottom-right corner; the rectangle matches the template
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - template.Width - 20, page.ActualSize.Height - template.Height - 20),
size: template.Size
});
// Create the stamp annotation and set the template as its appearance
let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
let appearance = new pdfModule.PdfAppearance(stamp);
appearance.Normal = template;
stamp.Appearance = appearance;
// Add the stamp annotation to the page's annotation collection
page.AnnotationsWidget.Add(stamp);
// Save the document
const outputFileName = 'DateTimeStamp.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>Add a Date and Time Stamp</h1>
<button onClick={addDateTimeStamp}>
Add Stamp
</button>
</div>
);
}
export default App;
A stamp in the page corner recording the current date and time:

FAQ
The stamp is too large and its position is off
Cause: A stamp's position and size on the page come from the rectangle of PdfRubberStampAnnotation, and the appearance fills that rectangle. When the template size and the rectangle size differ, the appearance is stretched; when the rectangle uses the image's pixel dimensions without scaling, the stamp can cover the body text or run off the page.
Solution: Work out the target display size first, give the template and the rectangle the same width and height, then position them relative to the page size:
// Give the template and the rectangle the same width and height
let template = new pdfModule.PdfTemplate({ width: stampWidth, height: stampHeight });
let rect = new pdfModule.RectangleF({
location: new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40, page.ActualSize.Height - stampHeight - 40),
size: template.Size
});
Text in the stamp renders as hollow boxes
Cause: PdfTrueTypeFont loads a font file through fontFile. If that file does not contain the glyphs for the text (for example, a Western font used with non-Latin characters), those characters have no glyph to draw and render as a row of boxes.
Solution: Use a font that contains the glyphs, and load the file into the VFS font directory /Library/Fonts/ first:
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12 });
How to customize the date and time format
Cause: DrawString draws exactly the string you pass in, so the format is decided entirely by the code that builds the string. Methods such as toLocaleString depend on the browser's locale, so the output can differ from one environment to another.
Solution: For a fixed format, build the string from the Date getters:
const now = new Date();
const pad = (value) => String(value).padStart(2, '0');
const timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
Get a Free License
If you want to remove the evaluation message from the resulting documents, or to get rid of the functional limitations, please contact sales for a temporary license valid for 30 days.
ET is WPS Office's workbook format, and its files carry the .et extension. When workbooks are exchanged between office suites, format compatibility decides whether a file can be opened directly. Spire.XLS for JavaScript converts between Excel and ET both ways in the browser on WebAssembly, keeping the formulas, the merged areas and the number formats of the workbook, and handling input and output files through a virtual file system (VFS) with no backend service.
This article covers two core feature points:
For installation and project setup, see Integrating Spire.XLS for JavaScript in a React Project. The examples below assume Spire.XLS is installed and the WebAssembly module has been initialised.
Convert Excel to ET
Saving an Excel workbook as ET makes the workbook open directly in WPS. The conversion changes only the format the file is saved in: the content of the workbook is unchanged, the target format is named by a fileFormat argument on the way out, and the formulas, the merged areas and the number formats of the cells are all kept. The steps are:
- Load the font and the test data file into the VFS.
- Load the Excel workbook with
workbook.LoadFromFile. - Save the workbook with
workbook.SaveToFile, namingxlsModule.FileFormat.ETas thefileFormat. - Dispose of the workbook with
workbook.Disposeto free resources.
The complete code example below shows how to convert an Excel workbook to ET in React:
function App() {
const excelToEt = async () => {
// get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// check that the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ProductList.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// save as ET; the format is named by fileFormat
const outputFileName = 'ExcelToEt.et';
workbook.SaveToFile({ fileName: outputFileName, fileFormat: xlsModule.FileFormat.ET });
// dispose of the workbook to free resources
workbook.Dispose();
// read the result file from the VFS and start the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.wps-office.et' });
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 Between Excel and ET in React</h1>
<button id="excel-to-et" onClick={excelToEt}>Convert Excel to ET</button>
</div>
);
}
export default App;
After running, the effect of converting an Excel workbook to an ET file:

Convert ET to Excel
Converting an ET file to Excel is the same operation in the other direction. ET and Excel hold the same workbook content and differ only in the file format, so loading the ET and writing it out as an Excel version is all it takes. The steps are:
- Load the font and the test data file into the VFS.
- Load the ET file with
workbook.LoadFromFile. - Save it as an Excel workbook with
workbook.SaveToFile, namingxlsModule.FileFormat.Version2010as thefileFormat. - Dispose of the workbook with
workbook.Disposeto free resources.
The complete code example below shows how to convert an ET file back to Excel in React:
function App() {
const etToExcel = async () => {
// get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// check that the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ProductList.et';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// save as Excel, again naming the version through fileFormat
const outputFileName = 'EtToExcel.xlsx';
workbook.SaveToFile({ fileName: outputFileName, fileFormat: xlsModule.FileFormat.Version2010 });
// dispose of the workbook to free resources
workbook.Dispose();
// read the result file from the VFS and start the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Between Excel and ET in React</h1>
<button id="et-to-excel" onClick={etToExcel}>Convert ET to Excel</button>
</div>
);
}
export default App;
After running, the effect of converting an ET file back to an Excel workbook:

FAQ
Can a file be saved as ET by naming it .et and leaving fileFormat out?
Cause: fileFormat is an optional argument of SaveToFile. When it is not passed, the format is taken from the extension of the output file name, so a name the library recognises determines the format.
Solution: Leaving it out works. Saving a .xlsx with workbook.SaveToFile({ fileName: 'ProductList.et' }) writes an ET file all the same. Writing fileFormat out is still the clearer of the two, and it keeps the output from landing in some other format when the name carries no extension or the wrong one.
Do the formulas in the workbook turn into fixed values after converting to ET?
Cause: ET and Excel carry the same workbook object, and the conversion only changes the format; nothing goes near the contents of the cells.
Solution: The formulas are kept as they are. In this example, reading the ET file that was written back in, the amount cell still has HasFormula set to true, its Formula is still =C3*D3, and FormulaNumberValue gives 58880 as the result; the merged area is still five columns wide, and the #,##0.00 number format is still in place. Formulas, formatting and layout survive the conversion in both directions.
Get a Free License
Spire.XLS for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.

JSON is widely used to exchange data between web applications and APIs, but it is not always the most convenient format for reviewing or sharing records. Exporting JSON to Excel turns that data into a worksheet that users can sort, edit, and use in reports.
This tutorial explains how to convert JSON to Excel in a React application using JavaScript and Spire.XLS for JavaScript. It covers both a simple array of employee records and nested JSON that must be flattened before export. You will also learn how to preserve basic data types, format the worksheet, and download the resulting XLSX file in the browser.
Table of Contents:
- Set Up Spire.XLS for JavaScript in React
- Convert a Simple JSON Array to Excel in React
- Convert Nested JSON to Excel in React
- Common Issues and Solutions
- FAQs
- Conclusion
1. Set Up Spire.XLS for JavaScript in React
Spire.XLS for JavaScript provides APIs for creating workbooks, writing cell values, applying styles, and saving Excel files. In this workflow, JavaScript parses and prepares the JSON data, while Spire.XLS builds the Excel workbook.
In your React project, install the package:
npm i spire.office
Copy the corresponding runtime files from the package into your project's public folder: spire.xls.js, Spire.Xls.Wasm.zip, spire.common.js, Spire.Common.Wasm.zip, and the _framework folder. Keep these files from the same package version. For detailed setup instructions, see How to Integrate Spire.XLS for JavaScript in a React Project.
Place the JSON input files in public as well. The examples below use employees.json and employees_nested.json.
The React component loads spire.xls.js inside useEffect() and keeps the initialized module in state. The conversion button remains disabled until initialization completes.
Project configuration: These examples follow the process.env.PUBLIC_URL and webpack-based loading convention in the supplied React component. If your project uses Vite or another build tool, adapt the public asset URL and dynamic import configuration to that environment.
2. Convert a Simple JSON Array to Excel in React
A flat array of objects maps naturally to a worksheet: property names become column headers, and each object becomes one data row.
Prepare the JSON File
Save the following content as public/employees.json:
[
{
"employee_id": "E001",
"name": "Jane Doe",
"department": "HR",
"salary": 85000.5,
"active": true,
"hire_date": "2022-03-15"
},
{
"employee_id": "E002",
"name": "Michael Smith",
"department": "IT",
"salary": 96000,
"active": true,
"hire_date": "2021-07-01"
},
{
"employee_id": "E003",
"name": "Sara Lin",
"department": "Finance",
"salary": 92000.75,
"active": false,
"hire_date": "2023-01-12"
},
{
"employee_id": "E004",
"name": "李伟",
"department": "Operations",
"salary": 81500,
"active": true,
"hire_date": "2020-11-30"
},
{
"employee_id": "E005",
"name": "Anna Petrova",
"department": "Marketing",
"salary": 78000,
"active": true,
"hire_date": "2019-08-19"
},
{
"employee_id": "E006",
"name": "David Chen",
"department": null,
"salary": 0,
"active": false
}
]
This example includes strings, numbers, Boolean values, Chinese characters, a null value, and a missing property. These cases illustrate how the export handles different values without losing zero or false.
Convert JSON to Excel
The conversion follows these steps:
- Load the JSON file into the virtual file system (VFS), decode its UTF-8 content, and parse it with
JSON.parse(). - Create a workbook, remove its default worksheets, and add an
Employeesworksheet. - Write the first object's keys into the header row.
- Write each record to a new row, using
NumberValue,BooleanValue, orTextaccording to the JavaScript value type. - Style the headers, estimate column widths from the content, and add thin borders.
- Save the workbook to the VFS, read the generated bytes, and download them as an XLSX file.
Complete React Example
Replace the contents of App.js with the following component:
import React, { useState, useEffect } from 'react';
function App() {
const [wasmModule, setWasmModule] = useState(null);
// Load Spire.XLS
useEffect(() => {
(async () => {
try {
const publicUrl = process.env.PUBLIC_URL || '';
const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.xls.js`);
const rawModule = spireModule.default || spireModule;
window.wasmModule = typeof rawModule === 'function'
? await rawModule({ locateFile: p => p.endsWith('.wasm') ? `${publicUrl}/${p}` : p })
: rawModule;
setWasmModule(window.wasmModule);
} catch (error) {
console.error('Failed to load spire.xls.js WASM module:', error);
}
})();
}, []);
// Convert JSON data to an Excel file
const JsonToExcel = async () => {
const spirexls = wasmModule?.spirexls;
if (!spirexls) {
console.error('Spire.XLS is not initialized.');
return;
}
let workbook;
try {
// 1. Load the JSON file into the Virtual File System (VFS)
const inputFileName = 'employees.json';
const inputVfsPath = await window.spire.FetchFileToVFS(
inputFileName,
'/',
`${process.env.PUBLIC_URL || ''}/`
);
// 2. Read it back as UTF-8 text and parse it into JS objects
const jsonBytes = window.dotnetRuntime.Module.FS.readFile(inputVfsPath);
const jsonText = new TextDecoder('utf-8').decode(jsonBytes);
const data = JSON.parse(jsonText);
// Guard against empty / invalid input
if (!Array.isArray(data) || data.length === 0) {
throw new Error('JSON must be a non-empty array of objects.');
}
// 3. Create a new workbook and worksheet
workbook = new spirexls.Workbook();
workbook.Worksheets.Clear();
const sheet = workbook.Worksheets.Add('Employees');
// 4. Write headers (row 1) from the keys of the first record
const headers = Object.keys(data[0]);
headers.forEach((header, col) => {
const cell = sheet.get(1, col + 1); // 1-based
cell.Text = header;
cell.Style.Font.IsBold = true;
cell.Style.Font.Size = 12;
cell.Style.Font.Color = spirexls.Color.get_White();
cell.Style.Color = spirexls.Color.get_DarkBlue();
});
// 5. Write data rows (from row 2), keeping the correct cell types
data.forEach((record, rowIdx) => {
headers.forEach((key, colIdx) => {
const cell = sheet.get(rowIdx + 2, colIdx + 1);
const value = record[key];
if (typeof value === 'number') {
cell.NumberValue = value;
} else if (typeof value === 'boolean') {
cell.BooleanValue = value;
} else {
cell.Text = value == null ? '' : String(value);
}
});
});
// 6. Set column widths explicitly
const widths = headers.map((header) => {
let maxLen = header.length;
data.forEach(record => {
const v = record[header];
maxLen = Math.max(maxLen, v == null ? 0 : String(v).length);
});
return Math.min(Math.max(maxLen + 4, 10), 40);
});
const columns = sheet.Columns;
widths.forEach((w, i) => { columns[i].ColumnWidth = w; }); // 0-based
// 7. Add thin borders to the used range
const usedRange = sheet.get(1, 1, data.length + 1, headers.length);
usedRange.Borders.LineStyle = spirexls.LineStyleType.Thin;
usedRange.Borders.Color = spirexls.Color.get_LightSteelBlue();
// 8. Save the workbook to the VFS
const outputFileName = 'employees.xlsx';
workbook.SaveToFile({
fileName: outputFileName,
version: spirexls.ExcelVersion.Version2016
});
// 9. Read the saved file, convert to a Blob and trigger download
const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const modifiedFile = new Blob([modifiedFileArray], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(modifiedFile);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to convert JSON to Excel:', error);
} finally {
// Clean up resources even when conversion fails part-way through.
try {
workbook?.Dispose();
} catch (cleanupError) {
console.warn('Failed to dispose workbook:', cleanupError);
}
}
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert JSON to Excel Using JavaScript in React</h1>
<button onClick={JsonToExcel} disabled={!wasmModule}>
Convert
</button>
</div>
);
}
export default App;
The code uses 1-based row and column indexes for sheet.get(), while the sheet.Columns collection is accessed with a 0-based index. Column widths are estimated from string length and capped at 40; this is a simple sizing rule rather than an exact measurement of rendered text.
Output:
The screenshot below shows the generated employees.xlsx file, with a styled header row and six employee records. Numbers and Boolean values retain their cell types, Chinese characters are preserved, and null or missing values appear as blank cells. Date strings remain in their original YYYY-MM-DD text format.

JSON has no native date type, and this code does not convert date strings into Excel dates. Also, the headers come from the first record: properties found only in later records are not included. The next example collects fields across all records to address that limitation.
3. Convert Nested JSON to Excel in React
Nested objects do not map directly to individual worksheet cells. Before exporting them, flatten their properties into a single level and decide how arrays should appear in the worksheet.
For this example, nested object paths become dot-separated column names, and arrays of simple values become comma-separated text. Each employee remains on one row.
Prepare a Nested JSON Example
Save the following content as public/employees_nested.json:
[
{
"employee_id": "E001",
"name": "Jane Doe",
"salary": 85000.5,
"active": true,
"address": {
"city": "Seattle",
"country": "USA"
},
"skills": ["Recruitment", "Training"]
},
{
"employee_id": "E002",
"name": "Michael Smith",
"salary": 96000,
"active": true,
"address": {
"city": "London",
"country": "UK",
"postal_code": "SW1A 1AA"
},
"skills": ["JavaScript", "React"]
},
{
"employee_id": "E004",
"name": "李伟",
"salary": 81500,
"active": false,
"address": {
"city": "Shanghai",
"country": "China"
},
"skills": []
}
]
The postal_code property appears only in the second employee's address. The export must therefore gather the column names from all flattened records, rather than using only the first one.
Flatten Nested Objects and Handle Arrays
Add these helper functions above function App() in the existing App.js file:
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function flattenRecord(record) {
// A null-prototype object safely holds arbitrary JSON property names.
const result = Object.create(null);
const visit = (value, path) => {
if (Array.isArray(value)) {
const containsComplexValues = value.some(
item => item !== null && typeof item === 'object'
);
// Preserve complex arrays as JSON text; join simple arrays for readability.
result[path] = containsComplexValues
? JSON.stringify(value)
: value.map(item => item == null ? '' : String(item)).join(', ');
} else if (isRecord(value)) {
const entries = Object.entries(value);
if (entries.length === 0 && path) {
result[path] = '';
} else {
entries.forEach(([key, child]) => {
visit(child, path ? `${path}.${key}` : key);
});
}
} else {
result[path] = value;
}
};
visit(record, '');
return result;
}
For the first employee, the resulting object is equivalent to:
{
"employee_id": "E001",
"name": "Jane Doe",
"salary": 85000.5,
"active": true,
"address.city": "Seattle",
"address.country": "USA",
"skills": "Recruitment, Training"
}
Numbers and Boolean values remain unchanged outside arrays. Empty arrays become empty text. If an array contains objects or other arrays, the helper stores it as JSON text instead of producing an unhelpful [object Object] string.
This dot-separated naming convention assumes that the original property names do not contain dots. If your data includes literal keys such as "address.city", use an escaped path convention or an explicit column mapping to avoid collisions. Array-to-text conversion is intended for readable export, not lossless reconstruction of the original JSON.
Export the Flattened Data to Excel
Keep the module initialization and component layout from the simple example. Replace its JsonToExcel handler with the following handler. It loads the nested JSON file, flattens each record, collects every available field, and exports the resulting table:
const JsonToExcel = async () => {
const spirexls = wasmModule?.spirexls;
if (!spirexls) {
console.error('Spire.XLS is not initialized.');
return;
}
let workbook;
try {
// 1. Load and parse the nested JSON file
const inputVfsPath = await window.spire.FetchFileToVFS(
'employees_nested.json',
'/',
`${process.env.PUBLIC_URL || ''}/`
);
const bytes = window.dotnetRuntime.Module.FS.readFile(inputVfsPath);
const data = JSON.parse(new TextDecoder('utf-8').decode(bytes));
if (!Array.isArray(data) || data.length === 0 || !data.every(isRecord)) {
throw new Error('JSON must be a non-empty array of objects.');
}
// 2. Flatten records and collect fields from every record
const flatData = data.map(flattenRecord);
const headers = [...new Set(flatData.flatMap(record => Object.keys(record)))];
if (headers.length === 0) {
throw new Error('JSON records contain no exportable fields.');
}
// 3. Create a workbook and worksheet
workbook = new spirexls.Workbook();
workbook.Worksheets.Clear();
const sheet = workbook.Worksheets.Add('Employees');
// 4. Write and style the headers
headers.forEach((header, col) => {
const cell = sheet.get(1, col + 1);
cell.Text = header;
cell.Style.Font.IsBold = true;
cell.Style.Font.Size = 12;
cell.Style.Font.Color = spirexls.Color.get_White();
cell.Style.Color = spirexls.Color.get_DarkBlue();
});
// 5. Write flattened values, preserving scalar types
flatData.forEach((record, row) => {
headers.forEach((key, col) => {
const cell = sheet.get(row + 2, col + 1);
const value = record[key];
if (typeof value === 'number') {
cell.NumberValue = value;
} else if (typeof value === 'boolean') {
cell.BooleanValue = value;
} else {
cell.Text = value == null ? '' : String(value);
}
});
});
// 6. Estimate column widths and add borders
const columns = sheet.Columns;
headers.forEach((header, col) => {
let maxLen = header.length;
flatData.forEach(record => {
const value = record[header];
maxLen = Math.max(maxLen, value == null ? 0 : String(value).length);
});
columns[col].ColumnWidth = Math.min(Math.max(maxLen + 4, 10), 40);
});
const usedRange = sheet.get(1, 1, flatData.length + 1, headers.length);
usedRange.Borders.LineStyle = spirexls.LineStyleType.Thin;
usedRange.Borders.Color = spirexls.Color.get_LightSteelBlue();
// 7. Save and download the workbook
const outputFileName = 'employees_nested.xlsx';
workbook.SaveToFile({
fileName: outputFileName,
version: spirexls.ExcelVersion.Version2016
});
const outputBytes = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([outputBytes], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = outputFileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to convert nested JSON to Excel:', error);
} finally {
try {
workbook?.Dispose();
} catch (cleanupError) {
console.warn('Failed to dispose workbook:', cleanupError);
}
}
};
Output:

The generated worksheet includes address.city, address.country, and address.postal_code as separate columns. Skills appear as readable text in one cell, and employees without a postal code have an empty cell in that column. Header order follows the first appearance of each field across the records.
For arrays of objects that need to be analyzed as individual items, a separate worksheet or an explicitly defined one-to-many row expansion is usually more useful than storing JSON text. That approach requires deciding which parent fields to repeat and how related records should be linked.
4. Common Issues and Solutions
| Issue | Solution |
|---|---|
| The conversion button stays disabled | Check the browser console and Network panel for module-loading errors. Verify that the runtime files are accessible and come from the same package version. |
| The JSON file cannot be loaded | Confirm that it is in public and that the public URL points to the correct location. An HTML error page returned instead of JSON can also cause a parse error. |
JSON.parse() throws an error |
Check for malformed JSON, comments, trailing commas, or an unexpected response body. |
| Input is empty or has the wrong structure | Provide a non-empty array of objects. For API responses wrapped in an object, select the array property before exporting. |
| Some columns are missing | Collect the union of keys from all records, as shown in the nested example, instead of reading only data[0]. |
A cell contains [object Object] |
Flatten nested objects before writing them, or deliberately serialize complex values with JSON.stringify(). |
| Dates behave like text in Excel | The examples write date strings as text. Add explicit date parsing and Excel date formatting if date calculations are required. |
The simple example validates that the input is a non-empty array but does not verify each member's type. For less predictable input, use the stronger data.every(isRecord) validation from the nested example and reject records with no usable fields.
5. FAQs
Can I convert JSON from an API response to Excel?
Yes. Use fetch() and response.json() to obtain the data, then pass the resulting array through the same worksheet-writing process. If the response has a wrapper such as { "employees": [...] }, select responseData.employees first. Loading a static JSON file into the VFS is not necessary when the data is already available as JavaScript objects.
How do I handle records with different fields?
Build the column list from every record's keys. The nested example uses Set to collect unique field names in their first-seen order. When a record does not contain a column's field, the code writes empty text to that cell.
How can I export nested objects and arrays?
Flatten nested objects into path-based columns, such as address.city. For simple arrays, joining values into one cell keeps each parent record on one row. Arrays of objects can be stored as JSON text, expanded into multiple rows, or exported to a separate worksheet, depending on how users need to work with the data.
Are numbers and Boolean values preserved in Excel?
Yes. These examples assign JavaScript numbers to NumberValue and Boolean values to BooleanValue. Strings remain text, including numeric-looking identifiers and date strings. The null check uses value == null, so valid values such as 0 and false are not replaced with blanks.
6. Conclusion
Converting JSON to Excel in React involves preparing the data, writing it to a worksheet, and downloading the generated workbook. Flat JSON arrays can be exported directly, while nested data needs a defined flattening strategy. With type-aware cell assignments and consistent field collection, both workflows produce readable Excel files for sharing and further analysis.
Add a Header and Footer When Creating a New PDF in React with JavaScript
2026-10-09 05:32:11 Written by Nina TangLaying out a report, a form or a set of notes and exporting it as a PDF often starts from a blank page. Headers and footers belong at the moment the document is built: going back to add them after the body is finished leaves positions and pagination awkward to manage. Doing it by hand page after page is tedious, and handing the job to a server means the data leaves the user's device.
This article uses Spire.PDF for JavaScript to create a PDF from scratch and add a header and a footer. The page margins are set to 0, and the surrounding white space comes from the Left, Top, Right and Bottom template elements of the document template (PdfDocumentTemplate) instead; the header and footer are drawn inside the top and bottom templates and appear on every new page at render time. The footer can fill in page numbers dynamically with PdfCompositeField. The library creates, draws and saves documents in the browser on top of WebAssembly, reading and writing files through a virtual file system (VFS), with no backend involved.
Two core features are covered:
For installation and project setup, see Integrating Spire.PDF for JavaScript into a React project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.
Add a header when creating a new PDF
Spire.PDF for JavaScript provides PdfPageTemplateElement for reserving a content area on a page that you can draw into independently and that travels with the page. Assign it to PdfDocumentTemplate.Top and every new page carries this header template inside its top margin. The template element's height is the top margin; inside it, coordinates for lines, text and images start at the page's top-left corner, with x to the right and y downward.
function App() {
const addHeaderToNewPdf = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check that the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the font and the header image into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const imageFileName = 'E-iceblue.png';
await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a new PDF document with an A4 page size
const doc = new pdfModule.PdfDocument();
const pageSize = pdfModule.PdfPageSize.A4();
doc.PageSettings.Size = pageSize;
// Set the page margins to 0; the white space comes from the document template's edge elements (50 points each)
doc.PageSettings.Margins = new pdfModule.PdfMargins({ margin: 0 });
const margins = new pdfModule.PdfMargins({ margin: 50 });
// Reserve the left, right and bottom edges with blank templates so the body area is inset
doc.Template.Left = new pdfModule.PdfPageTemplateElement({ width: margins.Left, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Right = new pdfModule.PdfPageTemplateElement({ width: margins.Right, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Bottom = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Bottom });
// Header template: its height is the top margin; Foreground = true draws it above the body
const headerSpace = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
headerSpace.Foreground = true;
doc.Template.Top = headerSpace;
// Font, brush, pen and alignment for the header
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 10, style: pdfModule.PdfFontStyle.Italic });
const brush = pdfModule.PdfBrushes.get_Gray();
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Gray(), width: 1.0 });
const leftAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left });
// Read the header image and convert its pixel size to points
const headerImage = pdfModule.PdfImage.FromFile(imageFileName);
const unitCvtr = new pdfModule.PdfUnitConvertor();
const pointWidth = unitCvtr.ConvertUnits(headerImage.Width, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
const pointHeight = unitCvtr.ConvertUnits(headerImage.Height, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
// Put the logo on the right of the header, two lines of text on the left, and a divider line at the bottom
headerSpace.Graphics.DrawImage({
image: headerImage,
x: headerSpace.Width - margins.Left - pointWidth,
y: headerSpace.Height - pointHeight
});
headerSpace.Graphics.DrawString({
s: 'E-iceblue Co., Ltd.\nwww.e-iceblue.com',
font: font,
brush: brush,
x: margins.Left,
y: headerSpace.Height - font.Height * 2,
format: leftAlign
});
headerSpace.Graphics.DrawLine({
pen: pen,
x1: margins.Left,
y1: margins.Top,
x2: pageSize.Width - margins.Left,
y2: margins.Top
});
// Add pages and write the body text; the header is applied by the template at render time
const bodyFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 14 });
const bodyBrush = pdfModule.PdfBrushes.get_Blue();
const texts = ['Spire.PDF Header Example', 'Second page body text'];
for (let i = 0; i < texts.length; i++) {
const page = doc.Pages.Add();
page.Canvas.DrawString({ s: texts[i], font: bodyFont, brush: bodyBrush, x: margins.Left, y: margins.Top + 30 });
}
const outputFileName = 'Header_Example.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger a 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>Add a header when creating a new PDF</h1>
<button onClick={addHeaderToNewPdf}>
Generate
</button>
</div>
);
}
export default App;
Every new page carries the header at the top:

Add a footer when creating a new PDF
The footer hangs off PdfDocumentTemplate.Bottom; the approach matches the header, only the template height changes to the bottom margin. A footer often carries a page number: PdfPageNumberField and PdfPageCountField combine into a PdfCompositeField, and the number resolves to whichever page it is on.
function App() {
const addFooterToNewPdf = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check that the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the font into the VFS for the footer text and page numbers
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a new PDF document with an A4 page size
const doc = new pdfModule.PdfDocument();
const pageSize = pdfModule.PdfPageSize.A4();
doc.PageSettings.Size = pageSize;
// Set the page margins to 0; the white space comes from the document template's edge elements (50 points each)
doc.PageSettings.Margins = new pdfModule.PdfMargins({ margin: 0 });
const margins = new pdfModule.PdfMargins({ margin: 50 });
// Reserve the left, right and top edges with blank templates so the body area is inset
doc.Template.Left = new pdfModule.PdfPageTemplateElement({ width: margins.Left, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Right = new pdfModule.PdfPageTemplateElement({ width: margins.Right, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Top = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
// Footer template: its height is the bottom margin; Foreground = true draws it above the body
const footerSpace = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Bottom });
footerSpace.Foreground = true;
doc.Template.Bottom = footerSpace;
// Font, brush, pen and alignment for the footer
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 12, style: pdfModule.PdfFontStyle.Italic });
const brush = pdfModule.PdfBrushes.get_Gray();
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Gray(), width: 1.0 });
const leftAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left });
// Draw a divider line at the top of the footer, with two lines of contact details left-aligned below it
footerSpace.Graphics.DrawLine({ pen: pen, x1: margins.Left, y1: 0, x2: pageSize.Width - margins.Left, y2: 0 });
footerSpace.Graphics.DrawString({
s: 'Email: sales@e-iceblue.com\nTel: 028-81705109',
font: font,
brush: brush,
x: margins.Left,
y: 2,
format: leftAlign
});
// Two dynamic fields: the current page number and the total page count
const number = new pdfModule.PdfPageNumberField();
const count = new pdfModule.PdfPageCountField();
// Combine them into "Page {0} of {1}" in placeholder order; the list order must match {0} and {1}
const compositeField = new pdfModule.PdfCompositeField({
font: font,
brush: brush,
text: 'Page {0} of {1}',
list: [number, count]
});
compositeField.StringFormat = new pdfModule.PdfStringFormat({
alignment: pdfModule.PdfTextAlignment.Right,
lineAlignment: pdfModule.PdfVerticalAlignment.Top
});
// Measure the page-number text so it sits right-aligned at the end of the footer line
const size = font.MeasureString({ text: compositeField.Text });
compositeField.Bounds = new pdfModule.RectangleF({
x: pageSize.Width - margins.Left - size.Width,
y: 2,
width: size.Width,
height: size.Height
});
compositeField.Draw({ graphics: footerSpace.Graphics });
// Add pages and write the body text; the footer is applied by the template at render time
const bodyFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 14 });
const bodyBrush = pdfModule.PdfBrushes.get_Blue();
const texts = ['Spire.PDF Footer Example', 'Second page body text'];
for (let i = 0; i < texts.length; i++) {
const page = doc.Pages.Add();
page.Canvas.DrawString({ s: texts[i], font: bodyFont, brush: bodyBrush, x: margins.Left, y: margins.Top + 30 });
}
const outputFileName = 'Footer_Example.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger a 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>Add a footer when creating a new PDF</h1>
<button onClick={addFooterToNewPdf}>
Generate
</button>
</div>
);
}
export default App;
Every new page carries the footer at the bottom:

FAQ
The body text touches the page edge and overlaps the header or footer
Cause: the page margins are set to 0, so the body area's white space comes entirely from the Left, Top, Right and Bottom template elements of doc.Template. Attach a template only on the header or footer side and leave the other sides empty, and the body runs right up to that page edge.
Solution: attach a template element on all four sides; use a blank template where there is no content to place:
doc.Template.Left = new pdfModule.PdfPageTemplateElement({ width: margins.Left, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Right = new pdfModule.PdfPageTemplateElement({ width: margins.Right, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Top = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
doc.Template.Bottom = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Bottom });
Chinese characters in the header or footer show up as boxes
Cause: the template text uses a Western font without Chinese glyphs (such as ARIAL.TTF), so no matching glyph is found at render time and each character becomes a hollow box.
Solution: switch to a font that includes Chinese, and load it into the virtual file system's /Library/Fonts/ first:
// Load the Chinese font first, then hand it to PdfTrueTypeFont
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 10 });
The header or footer is hidden behind the body text
Cause: PdfPageTemplateElement.Foreground defaults to false, so the template is drawn beneath the body. If the body reaches the same position, it covers the header or footer.
Solution: set the template element's Foreground to true so it is drawn above the body:
const headerSpace = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
headerSpace.Foreground = true;
doc.Template.Top = headerSpace;
Get a Free License
If you want to remove the evaluation message from the result document, or lift the feature limits, contact sales for a temporary license valid for 30 days.
Add Header and Footer to an Existing PDF with JavaScript in React
2026-10-09 03:15:34 Written by Nina TangContracts and reports that go out to others usually carry a header and a footer beyond the body: the header says whose document this is, and the footer holds the page number and a note like "Internal use only". Typing them in by hand is slow and easy to miss, and handing the job to a server means the document has to leave the user's device first.
This article shows how to use Spire.PDF for JavaScript to add a text header and footer, an image header and footer, and automatic page numbers to an existing PDF. It loads, modifies and saves PDF documents directly in the browser with WebAssembly, draws the header and footer inside the blank areas the page already has, and reads and writes files through the virtual file system (VFS), with no backend involved.
This article covers three core features:
- Add a text header and footer
- Add an image to the header and footer
- Add automatic page numbers and total page count
For installation and project setup, see Integrate Spire.PDF for JavaScript into a React project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.
Coordinate system
On an existing PDF page, the coordinate system is set up as follows: the origin is at the top-left corner of the page, the X axis extends to the right, and the Y axis extends downward (as shown below). Values are in points, and the drawing coordinates in the three features below all follow this convention.

Add a text header and footer
To give every page a fixed header and footer, draw the text at the target coordinates with DrawString on PdfPageBase.Canvas.
function App() {
const addTextHeaderFooter = 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 font into the VFS for the header and footer text
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the input PDF into the VFS
const inputFileName = 'Multipage_Document.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Page margins, plus the pen, font and alignment shared by header and footer
const margin = doc.PageSettings.Margins;
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Black(), width: 0.75 });
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 10 });
const brush = pdfModule.PdfBrushes.get_Black();
const rightAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Right });
rightAlign.MeasureTrailingSpaces = true;
// Gap between the separator line and the text
const space = font.Height * 0.75;
// Draw the header and footer page by page
for (let i = 0; i < doc.Pages.Count; i++) {
const canvas = doc.Pages.get_Item(i).Canvas;
const x = margin.Left;
const width = canvas.ClientSize.Width - margin.Left - margin.Right;
// Header: draw the separator above the content area, right-align the title above the line
let y = margin.Top - space;
canvas.DrawLine({ pen: pen, x1: x, y1: y, x2: x + width, y2: y });
canvas.DrawString({
s: 'E-iceblue',
font: font,
brush: brush,
x: x + width,
y: y - font.Height,
format: rightAlign
});
// Footer: draw the separator below the content area, right-align the note below the line
y = canvas.ClientSize.Height - margin.Bottom + space;
canvas.DrawLine({ pen: pen, x1: x, y1: y, x2: x + width, y2: y });
canvas.DrawString({
s: 'Internal use only',
font: font,
brush: brush,
x: x + width,
y: y + 1,
format: rightAlign
});
}
const outputFileName = 'Text_Header_Footer.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>Add a Text Header and Footer</h1>
<button onClick={addTextHeaderFooter}>
Start Adding
</button>
</div>
);
}
export default App;
Every page carries a right-aligned header and footer, each separated from the body by a separator line:

Add an image to the header and footer
A company logo often goes into the header and footer too. PdfImage.FromFile reads the image from the virtual file system, and Canvas.DrawImage draws it by its top-left coordinates. The image size is in pixels, so convert it into points with PdfUnitConvertor first to line it up with the page coordinates.
function App() {
const addImageHeaderFooter = 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 input PDF and the logo image into the VFS
const inputFileName = 'Multipage_Document.pdf';
const imageFileName = 'E-iceblue.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
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Read the logo image and convert its pixel size into points
const image = pdfModule.PdfImage.FromFile(imageFileName);
const unitCvtr = new pdfModule.PdfUnitConvertor();
const pointWidth = unitCvtr.ConvertUnits(image.Width, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
const pointHeight = unitCvtr.ConvertUnits(image.Height, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
// Draw the logo at the top-right of the header and the bottom-left of the footer, page by page
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
const canvas = page.Canvas;
// Top-right of the header
canvas.DrawImage({
image: image,
x: page.ActualSize.Width - pointWidth - 55,
y: 20
});
// Bottom-left of the footer
canvas.DrawImage({
image: image,
x: 55,
y: page.ActualSize.Height - pointHeight - 20
});
}
const outputFileName = 'Image_Header_Footer.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>Add an Image to the Header and Footer</h1>
<button onClick={addImageHeaderFooter}>
Start Adding
</button>
</div>
);
}
export default App;
The company logo lands at the top-right of the header and the bottom-left of the footer:

Add automatic page numbers and total page count
The product provides the PdfPageNumberField and PdfPageCountField classes to get the current page number and the total number of pages in a PDF document. They can be merged into a PdfCompositeField that formats the output as "Page X of Y", where X is the current page number and Y is the total page count.
function App() {
const addPageNumbers = 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 font into the VFS for the page number text
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the input PDF into the VFS
const inputFileName = 'Multipage_Document.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object and load the PDF document
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Page margins, plus the pen, font and alignment for the page number text
const margin = doc.PageSettings.Margins;
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Black(), width: 0.75 });
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL.TTF', size: 10 });
const brush = pdfModule.PdfBrushes.get_Black();
const rightAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Right });
rightAlign.MeasureTrailingSpaces = true;
const space = font.Height * 0.75;
// Two dynamic fields: the current page number and the total page count
const number = new pdfModule.PdfPageNumberField();
const count = new pdfModule.PdfPageCountField();
// Join them into "Page X of Y" by placeholder order; the list order matches {0} and {1}
const compositeField = new pdfModule.PdfCompositeField({
font: font,
brush: brush,
text: 'Page {0} of {1}',
list: [number, count]
});
compositeField.StringFormat = rightAlign;
// Draw the separator line and place the right-aligned page number, page by page
for (let i = 0; i < doc.Pages.Count; i++) {
const canvas = doc.Pages.get_Item(i).Canvas;
const x = margin.Left;
const width = canvas.ClientSize.Width - margin.Left - margin.Right;
// Draw a separator line below the content area
const y = canvas.ClientSize.Height - margin.Bottom + space;
canvas.DrawLine({ pen: pen, x1: x, y1: y, x2: x + width, y2: y });
// Measure the text and place the page number below the line, flush with the right edge
const size = font.MeasureString({ text: 'Page 1 of 1' });
compositeField.Bounds = new pdfModule.RectangleF({
x: x + width - size.Width,
y: y + 1,
width: size.Width,
height: size.Height
});
// Draw the dynamic field directly onto the canvas
compositeField.Draw({ graphics: canvas });
}
const outputFileName = 'Page_Number_Footer.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>Add Page Numbers</h1>
<button onClick={addPageNumbers}>
Start Adding
</button>
</div>
);
}
export default App;
The footer shows "Page X of Y" right-aligned below the separator line:

Frequently Asked Questions
The header and footer are drawn on top of the body text
Cause: The header and footer are layered onto the page, and their position comes from the coordinates derived from PageSettings.Margins. If the loaded PDF has very little blank space at the top and bottom, that text overlaps the body.
Solution: Save the original page as a template first, and after drawing the header and footer, draw the original content back into place so the body and the header/footer each take their own layer. If you don't need layers, use a source file with enough blank space at the top and bottom:
// Save the current page content as a template
const template = page.CreateTemplate();
// After drawing the header and footer, lay the original content back over the full page
const graphicsWidget = new pdfModule.PdfGraphicsWidget(template.H);
graphicsWidget.Draw({ graphics: canvas, location: new pdfModule.PointF(0, 0) });
The header and footer text shows up as boxes
Cause: The font has no glyphs for those characters. Rendering CJK text with a Latin font such as ARIAL.TTF finds no matching glyph, so the page shows empty boxes.
Solution: Switch to a font file that includes those glyphs, and load it into /Library/Fonts/ in the virtual file system first:
// Load a CJK-capable font first, then hand it to PdfTrueTypeFont
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 10 });
The page number numbers don't match
Cause: The text of PdfCompositeField uses {0} and {1} as placeholders, filled in the order given by list. Putting PdfPageCountField before PdfPageNumberField, or swapping the two placeholders in text, makes the page number and the total page count trade places.
Solution: Keep the placeholder order consistent with list: page number first, total page count second:
const compositeField = new pdfModule.PdfCompositeField({
font: font,
brush: brush,
text: 'Page {0} of {1}',
list: [number, count]
});
Get a Free License
If you want to remove the evaluation message from the resulting document, or get past the feature limits, contact sales for a temporary 30-day license.
TXT files are everywhere — exported logs, body text pasted out of Word, reports a system generates in bulk. To hand one to someone else you usually convert it to PDF first: a TXT has no notion of layout or paper, it just runs on and on, and the same file looks different on every device and in every editor. In the past you either opened it in a desktop program and saved as, or uploaded it to a server to convert — the former doesn't fit into a web flow, and the latter means the document leaves the user's machine.
This article shows how to convert TXT text to PDF with Spire.PDF for JavaScript. It runs on WebAssembly to generate and save PDF documents directly in the browser; the whole conversion happens locally, reading and writing files through a virtual file system (VFS), with no backend involved.
For installation and project setup, see Integrate Spire.PDF for JavaScript in a React project. The example below assumes Spire.PDF is installed and the WebAssembly module has been initialized.
Convert TXT Text to PDF
Spire.PDF for JavaScript provides PdfTextWidget to place a whole block of plain text onto a PDF page. Font, line spacing and alignment go to PdfStringFormat; when the text fills the current page, PdfTextLayout wraps and starts a new page on its own, so a long TXT needs no manual pagination.
function App() {
const convertTxtToPdf = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check that the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load a TrueType font into the virtual file system for the body text
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Load the TXT file to convert into the VFS
const inputFileName = 'Product_Introduction.txt';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Read the TXT bytes from the VFS and decode them to a string
const textBytes = window.dotnetRuntime.Module.FS.readFile(inputFileName);
const text = new TextDecoder('utf-8').decode(textBytes);
// Create the document, section and page
let doc = new pdfModule.PdfDocument();
let section = doc.Sections.Add();
let page = section.Pages.Add();
// Use the embedded TrueType font for the body, and set line spacing and left alignment
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 12 });
let format = new pdfModule.PdfStringFormat();
format.LineSpacing = 20;
format.Alignment = pdfModule.PdfTextAlignment.Left;
// Break to a new page when the page is full, so the overflow flows onto new pages
let textLayout = new pdfModule.PdfTextLayout();
textLayout.Break = pdfModule.PdfLayoutBreakType.FitPage;
textLayout.Layout = pdfModule.PdfLayoutType.Paginate;
// Start the text at the top-left of the page, leaving 20 points of room at the top
let bounds = new pdfModule.RectangleF({
location: new pdfModule.PointF(0, 20),
size: page.Canvas.ClientSize
});
// Hold the whole text in a PdfTextWidget and hand it to the layout engine
let textWidget = new pdfModule.PdfTextWidget({
text: text,
font: font,
brush: pdfModule.PdfBrushes.get_Black()
});
textWidget.StringFormat = format;
let layoutWidget = new pdfModule.PdfLayoutWidget(textWidget.H);
layoutWidget.Draw({ page: page, layoutRectangle: bounds, format: textLayout });
// Save the document
const outputFileName = 'Product_Introduction.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>Convert TXT Text to PDF</h1>
<button onClick={convertTxtToPdf}>
Start
</button>
</div>
);
}
export default App;
The generated PDF spreads the long TXT text across pages automatically:

FAQ
Chinese characters show up as boxes or blank after conversion
Cause: the Helvetica font PdfTextWidget uses by default is a PDF built-in Western font with Latin glyphs only. When it meets a Chinese character it has no glyph to use, and the character shows up blank or as a box on the page.
Solution: switch to PdfTrueTypeFont loaded from a font file that contains Chinese glyphs, and put that file into the VFS /Library/Fonts/ first:
// Read the font from the project's static directory into the VFS
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Point fontFile at the font path in the VFS
let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 12 });
Long text doesn't paginate and only fills one page
Cause: PdfTextWidget only draws inside the rectangle you pass in, and content that overflows is not moved to the next page by itself — whether it paginates is decided by PdfTextLayout.Layout.
Solution: set PdfTextLayout.Layout to PdfLayoutType.Paginate and Break to PdfLayoutBreakType.FitPage, then pass that object as the format argument to Draw:
let textLayout = new pdfModule.PdfTextLayout();
textLayout.Break = pdfModule.PdfLayoutBreakType.FitPage; // break as soon as a page is full
textLayout.Layout = pdfModule.PdfLayoutType.Paginate; // paginate automatically
layoutWidget.Draw({ page: page, layoutRectangle: bounds, format: textLayout });
Tab indents in the TXT don't line up in the PDF
Cause: the text you read out is plain characters, and a tab is just a \t with no information about how many character widths it spans. During layout every \t is treated as the same width, so indentation that lined up through \t or extra spaces in the original no longer lines up in the PDF.
Solution: for table-like content that needs exact alignment, pad it out with spaces or draw it as a table instead; for plain prose meant to be read, output it as is.
Get a Free License
If you want to remove the evaluation message from the result document, or lift the feature limits, contact sales for a temporary license valid for 30 days.
Set Excel Superscript and Subscript in React with JavaScript
2026-10-08 03:11:46 Written by liu taliaThe number of atoms in a chemical formula, the exponent in a mathematical expression, the square or cube on a unit symbol, the charge at the end of an ion — these characters are set one size smaller and shifted off the baseline, and that is how readers expect to find them. Typed at the same size as the surrounding text, they have to be inferred from context. Excel rich text lets individual characters inside a single cell carry their own formatting, and superscript and subscript are the two forms used most often. Spire.XLS for JavaScript performs this directly in the browser through WebAssembly, managing input and output files with a virtual file system (VFS) and requiring no backend service.
This article covers two key features:
For installation and project setup, see Integrating Spire.XLS for JavaScript in a React Project. The examples below assume Spire.XLS is already installed and the WebAssembly module has been initialized.
Set the Subscript of Text
Superscript and subscript are not whole-cell font properties: RichText.SetFont applies a font to a slice of the text inside a cell rather than to the cell as a whole. Three things have to line up — write the text as a rich-text run, prepare a font that carries the shift, then apply that font to the character range that should move. The steps are as follows:
- Load the font into the VFS.
- Create a workbook and get the first worksheet.
- Write the sample labels, then the chemical formulas with
RichText.Text. - Create a font object with
workbook.CreateFont()and setIsSubscripttotrue. - Apply the font to the digits with
RichText.SetFont(startIndex, endIndex, font), then save the workbook.
Here is a complete code example that sets the subscript of cell text in React:
function App() {
const setSubscript = async () => {
// Get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// Check if the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// Load the font into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a new workbook and get the first worksheet
const workbook = new xlsModule.Workbook();
const sheet = workbook.Worksheets.get(0);
// Write the header row and the sample labels
sheet.Range.get("A1").Text = "Item";
sheet.Range.get("B1").Text = "Result";
sheet.Range.get("A2").Text = "Water";
sheet.Range.get("A3").Text = "Sulfuric acid";
// Write the chemical formulas
const waterRange = sheet.Range.get("B2");
waterRange.RichText.Text = "H2O";
const acidRange = sheet.Range.get("B3");
acidRange.RichText.Text = "H2SO4";
// Create a font and set IsSubscript to true
const font = workbook.CreateFont();
font.IsSubscript = true;
// Apply the subscript font to the digits only: indices start at 0 and the range is inclusive
waterRange.RichText.SetFont(1, 1, font);
acidRange.RichText.SetFont(1, 1, font);
acidRange.RichText.SetFont(4, 4, font);
// Widen columns A and B so the rich text is fully visible
sheet.Range.get("A1:A3").ColumnWidth = 18;
sheet.Range.get("B1:B3").ColumnWidth = 18;
// Save the workbook
const outputFileName = "SetSubscript.xlsx";
workbook.SaveToFile({ fileName: outputFileName });
// Dispose of the workbook object to free resources
workbook.Dispose();
// Read the result file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Subscript</h1>
<button onClick={setSubscript}>Start</button>
</div>
);
}
export default App;
After running, the effect of setting the subscript of text:

Set the Superscript of Text
Superscript is the mirror image of subscript; the only difference is the direction the font shifts — up rather than down. Superscript suits powers and exponents — the squares in the Pythagorean theorem, the squared cm2 of an area unit, the cubed m3 of a volume unit. A single RichText.SetFont call covers one contiguous range, so text such as a2 + b2 = c2, with three characters to raise, takes three calls. The steps are as follows:
- Load the font into the VFS.
- Create a workbook and get the first worksheet.
- Write the sample labels, then the text that receives the formatting with
RichText.Text. - Create a font object with
workbook.CreateFont()and setIsSuperscripttotrue. - Apply the font to the characters that should be raised with
RichText.SetFont(startIndex, endIndex, font), then save the workbook.
Here is a complete code example that sets the superscript of cell text in React:
function App() {
const setSuperscript = async () => {
// Get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// Check if the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// Load the font into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a new workbook and get the first worksheet
const workbook = new xlsModule.Workbook();
const sheet = workbook.Worksheets.get(0);
// Write the header row and the sample labels
sheet.Range.get("A1").Text = "Item";
sheet.Range.get("B1").Text = "Result";
sheet.Range.get("A2").Text = "Pythagorean theorem";
sheet.Range.get("A3").Text = "Area unit";
sheet.Range.get("A4").Text = "Volume unit";
// Write the text that receives the formatting
const theoremRange = sheet.Range.get("B2");
theoremRange.RichText.Text = "a2 + b2 = c2";
const areaRange = sheet.Range.get("B3");
areaRange.RichText.Text = "cm2";
const volumeRange = sheet.Range.get("B4");
volumeRange.RichText.Text = "m3";
// Create a font and set IsSuperscript to true
const font = workbook.CreateFont();
font.IsSuperscript = true;
// Apply the superscript font to the characters that should be raised
theoremRange.RichText.SetFont(1, 1, font);
theoremRange.RichText.SetFont(6, 6, font);
theoremRange.RichText.SetFont(11, 11, font);
areaRange.RichText.SetFont(2, 2, font);
volumeRange.RichText.SetFont(1, 1, font);
// Widen columns A and B so the rich text is fully visible
sheet.Range.get("A1:A4").ColumnWidth = 18;
sheet.Range.get("B1:B4").ColumnWidth = 18;
// Save the workbook
const outputFileName = "SetSuperscript.xlsx";
workbook.SaveToFile({ fileName: outputFileName });
// Dispose of the workbook object to free resources
workbook.Dispose();
// Read the result file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Superscript</h1>
<button onClick={setSuperscript}>Start</button>
</div>
);
}
export default App;
After running, the effect of setting the superscript of text:

FAQ
How do I remove a superscript or subscript that has already been set?
Cause: Superscript and subscript are formats on individual characters. Calling SetFont again over the same range overwrites those characters with the new font's properties, so applying a font without the shift clears the effect.
Solution: Create a font with IsSubscript or IsSuperscript set to false, and call SetFont over the original range:
// Overwrite the subscript with a plain font
const normal = workbook.CreateFont();
normal.IsSubscript = false;
range.RichText.SetFont(1, 1, normal);
How can I tell whether a character is superscript when reading the file back?
Cause: The shift is a format on the character, not part of the cell's text. Range.Text, Range.Value and RichText.Text all return plain text, so they cannot show which characters carry it.
Solution: Read the font at the character's position and check its superscript and subscript properties. Positions count from 0, the same as with SetFont:
// Read the font at index 1 — the middle "2" in H2O
const font = range.RichText.GetFont(1);
console.log(font.IsSubscript); // true
console.log(font.IsSuperscript); // false
Get a Free License
Spire.XLS for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.
Highlight Duplicate, Average and Ranked Values in Excel in React
2026-10-08 03:11:06 Written by liu taliaSpotting the order numbers that were entered twice, the ratings that sit below the average, or the handful of largest and smallest orders in a table of several hundred rows is slow work by eye, and easy to get wrong. Conditional formatting hands that judgement to Excel: once a rule is written, the matching cells carry the color themselves, and the marks follow the data when it changes. This article covers the three most common kinds of rule — whether a value repeats, how it compares with the average, and where it ranks. Spire.XLS for JavaScript runs all of this in the browser on WebAssembly, managing input and output files through a virtual file system (VFS) with no backend service required.
This article covers three features:
- Highlight duplicate and unique values
- Highlight values above or below average
- Highlight top or bottom ranked values
For installation and project setup, see Integrating Spire.XLS for JavaScript in a React Project. The examples below assume Spire.XLS is installed and the WebAssembly module has been initialized.
Highlight duplicate and unique values
A conditional formatting rule hangs off a conditional format set: sheet.ConditionalFormats.Add() returns one set, you point it at the cells it applies to, and then add a single condition whose color you set. Duplicate and unique values are two sides of the same comparison — the first matches cells whose value appears more than once in the range, the second matches cells that appear exactly once — so they are usually written as a pair that brings both the repeated order numbers and the one-off ones into view. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook and get the worksheet.
- Add a conditional format set with
sheet.ConditionalFormats.Add()and point it at the order number column withAddRange. - Add a condition with
AddCondition, set itsFormatTypetoDuplicateValues, and set the fill color. - Add a second set on the same range in the same way, switch the type to
UniqueValues, set another fill color, and save the workbook.
The full code sample below shows how to highlight duplicate and unique values in React:
function App() {
const highlightDuplicateValues = async () => {
// Get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// Check whether the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// Load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ConditionalFormatData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// Load the workbook and take the first worksheet
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// Add one conditional format set, applied to the order number column
const duplicateFormats = sheet.ConditionalFormats.Add();
duplicateFormats.AddRange(sheet.Range.get("C2:C10"));
// Add a duplicate-value condition, filling order numbers that appear more than once in IndianRed
const duplicateCondition = duplicateFormats.AddCondition();
duplicateCondition.FormatType = xlsModule.ConditionalFormatType.DuplicateValues;
duplicateCondition.BackColor = xlsModule.Color.get_IndianRed();
// Add another conditional format set, applied to the same range
const uniqueFormats = sheet.ConditionalFormats.Add();
uniqueFormats.AddRange(sheet.Range.get("C2:C10"));
// Add a unique-value condition, filling order numbers that appear once in Yellow
const uniqueCondition = uniqueFormats.AddCondition();
uniqueCondition.FormatType = xlsModule.ConditionalFormatType.UniqueValues;
uniqueCondition.BackColor = xlsModule.Color.get_Yellow();
// Save the workbook
const outputFileName = "HighlightDuplicateValues.xlsx";
workbook.SaveToFile({ fileName: outputFileName });
// Dispose of the workbook object to free resources
workbook.Dispose();
// Read the result file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>Highlight Duplicate and Unique Values</h1>
<button onClick={highlightDuplicateValues}>Start</button>
</div>
);
}
export default App;
After running, the effect of highlighting duplicate and unique values:

Highlight values above or below average
The average is the yardstick most people reach for when judging a set of numbers. Conditional formatting has a condition built for exactly that comparison: call AddAverageCondition for the above-average or the below-average case, and Excel works out the average of the range itself, then compares each cell against it — one color above the line, another below. Excel computes the average when the file is opened, so editing the data reshuffles the marks along with it. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook and get the worksheet.
- Add a conditional format set with
sheet.ConditionalFormats.Add()and point it at the customer rating column withAddRange. - Call
AddAverageConditionwithAverageType.Belowand set the fill color. - Add a second set on the same range in the same way, pass
AverageType.Aboveinstead, set another fill color, and save the workbook.
The full code sample below shows how to highlight values above or below average in React:
function App() {
const highlightAverageValues = async () => {
// Get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// Check whether the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// Load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ConditionalFormatData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// Load the workbook and take the first worksheet
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// Add one conditional format set, applied to the customer rating column
const belowFormats = sheet.ConditionalFormats.Add();
belowFormats.AddRange(sheet.Range.get("E2:E10"));
// Add a below-average condition, filling with SkyBlue
const belowCondition = belowFormats.AddAverageCondition(xlsModule.AverageType.Below);
belowCondition.BackColor = xlsModule.Color.get_SkyBlue();
// Add another conditional format set, applied to the same range
const aboveFormats = sheet.ConditionalFormats.Add();
aboveFormats.AddRange(sheet.Range.get("E2:E10"));
// Add an above-average condition, filling with Orange
const aboveCondition = aboveFormats.AddAverageCondition(xlsModule.AverageType.Above);
aboveCondition.BackColor = xlsModule.Color.get_Orange();
// Save the workbook
const outputFileName = "HighlightAverageValues.xlsx";
workbook.SaveToFile({ fileName: outputFileName });
// Dispose of the workbook object to free resources
workbook.Dispose();
// Read the result file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>Highlight Above and Below Average Values</h1>
<button onClick={highlightAverageValues}>Start</button>
</div>
);
}
export default App;
After running, the effect of highlighting values above or below average:

Highlight top or bottom ranked values
Leaderboard questions — which two orders are the largest, which three teams have the lowest completion rate — are hard to answer row by row, but a rank condition only needs the number of places. AddTopBottomCondition ranks the values inside the range you set and colors the cells that come out highest or lowest; it takes two arguments: one decides whether to take the highest or the lowest, the other is how many. The ranking stays inside that range and ignores everything else on the sheet. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook and get the worksheet.
- Add a conditional format set with
sheet.ConditionalFormats.Add()and point it at the order amount column withAddRange. - Call
AddTopBottomConditionwithTopBottomType.Topand a count of 2, then set the fill color. - Add a second set on the same range in the same way, pass
TopBottomType.Bottominstead, set another fill color, and save the workbook.
The full code sample below shows how to highlight top or bottom ranked values in React:
function App() {
const highlightRankedValues = async () => {
// Get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// Check whether the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// Load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ConditionalFormatData.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// Load the workbook and take the first worksheet
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
const sheet = workbook.Worksheets.get(0);
// Add one conditional format set, applied to the order amount column
const topFormats = sheet.ConditionalFormats.Add();
topFormats.AddRange(sheet.Range.get("D2:D10"));
// Add a top-2 condition, filling the two largest orders with Red
const topCondition = topFormats.AddTopBottomCondition(xlsModule.TopBottomType.Top, 2);
topCondition.FormatType = xlsModule.ConditionalFormatType.TopBottom;
topCondition.BackColor = xlsModule.Color.get_Red();
// Add another conditional format set, applied to the same range
const bottomFormats = sheet.ConditionalFormats.Add();
bottomFormats.AddRange(sheet.Range.get("D2:D10"));
// Add a bottom-2 condition, filling the two smallest orders with ForestGreen
const bottomCondition = bottomFormats.AddTopBottomCondition(xlsModule.TopBottomType.Bottom, 2);
bottomCondition.FormatType = xlsModule.ConditionalFormatType.TopBottom;
bottomCondition.BackColor = xlsModule.Color.get_ForestGreen();
// Save the workbook
const outputFileName = "HighlightRankedValues.xlsx";
workbook.SaveToFile({ fileName: outputFileName });
// Dispose of the workbook object to free resources
workbook.Dispose();
// Read the result file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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>Highlight Top and Bottom Ranked Values</h1>
<button onClick={highlightRankedValues}>Start</button>
</div>
);
}
export default App;
After running, the effect of highlighting top or bottom ranked values:

FAQ
Why is the color I set not showing up?
Cause: The color belongs on the condition object, which is what AddCondition, AddAverageCondition or AddTopBottomCondition returns. Assigning BackColor to the outer conditional format set instead is accepted and raises no error, but the rule that reaches the file carries no fill and the cells stay as they were.
Solution: Assign the color to the condition object — the return value of those methods:
// The condition object is what carries the color
const formats = sheet.ConditionalFormats.Add();
formats.AddRange(sheet.Range.get("C2:C10"));
const condition = formats.AddCondition();
condition.FormatType = xlsModule.ConditionalFormatType.DuplicateValues;
condition.BackColor = xlsModule.Color.get_IndianRed();
Why can't I read a conditional format's fill color back from the cell?
Cause: The color of a conditional format does not belong to the cell's own formatting. It is stored with the rule in the file's conditional formatting settings, and the cell keeps the style it already had, so the color read from the cell is the same before and after the rule is added.
Solution: Read the color from the condition object — the return value of AddCondition, AddAverageCondition or AddTopBottomCondition:
// The conditional format color lives on the condition object, not in the cell style
const formats = sheet.ConditionalFormats.Add();
formats.AddRange(sheet.Range.get("C2:C10"));
const condition = formats.AddCondition();
condition.FormatType = xlsModule.ConditionalFormatType.DuplicateValues;
condition.BackColor = xlsModule.Color.get_IndianRed();
// This reads back the color that was just set
console.log(condition.BackColor);
Get a Free License
Spire.XLS for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.
Insert Pictures and Text Boxes in a Chart in React with JavaScript
2026-09-30 09:20:11 Written by liu taliaA chart makes the data clear; it says nothing about whose brand it belongs to, or what the one-line conclusion is. The usual fix in a report is a logo in one corner of the chart plus a note such as "Online: 1,208K USD in total" in the empty space — putting the conclusion where the reader's eye already is instead of starting another paragraph of prose. In Excel these elements belong to the chart's own shape layer, positioned against the chart rather than against the cells, and that is where code that adds them most often goes wrong. The plot area's default white background is a separate matter again: it can be swapped for a light texture so that the chart and the rest of the report look like one piece. Spire.XLS for JavaScript does all of this in the browser on top of WebAssembly, managing input and output files through a virtual file system (VFS), with no backend service required.
This article covers three key features:
For installation and project setup, see Integrating Spire.XLS for JavaScript in a React Project. The examples below assume Spire.XLS is installed and the WebAssembly module has been initialised.
Insert a picture into a chart
Charts in a report often need to carry a brand mark or a product shot. Once a picture is inside the chart it becomes part of it: move or resize the chart and the picture comes along, and copying the chart into another document or exporting it as an image keeps the picture too. A picture floating above the cells, by contrast, is out of alignment the moment the chart moves. The steps are:
- Load the font, the test data file and the picture into the VFS.
- Load the workbook with
workbook.LoadFromFileand take the first chart on the first worksheet. - Add the picture to the chart with
chart.Shapes.AddPicture; the value it returns is that shape. - Set the shape's
Left,Top,WidthandHeight. A shape inside a chart is measured against the chart itself: each of the four is in units of 1/4000 of the chart's width (Left,Width) or height (Top,Height). - Save the workbook with
workbook.SaveToFile.
The complete code example below shows how to insert a picture into a chart in React:
function App() {
const addPictureInChart = async () => {
// get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// check that the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// load the font, the test data file and the picture into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ChartReport.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
await window.spire.FetchFileToVFS('logo.png', '', `${process.env.PUBLIC_URL}static/image/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// take the first worksheet and the chart on it
const sheet = workbook.Worksheets.get(0);
const chart = sheet.Charts.get(0);
// insert the picture into the chart; the object returned is that picture
const picture = chart.Shapes.AddPicture('logo.png');
// place it in the top-right corner and scale it down. Left unset, the picture is laid out at
// its natural pixel size and usually covers most of the chart
picture.Left = 2850; // 2850/4000 from the left edge of the chart
picture.Top = 110; // 110/4000 from the top edge of the chart
picture.Width = 900; // width 900/4000
picture.Height = 532; // height 532/4000, matching the 320x120 of the source picture
// save the workbook
const outputFileName = 'AddPictureInChart.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// dispose of the workbook to free resources
workbook.Dispose();
// read the result file from the VFS and start the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Picture and a Text Box in a Chart</h1>
<button id="add-picture-in-chart" onClick={addPictureInChart}>Add a picture to the chart</button>
</div>
);
}
export default App;
After running, inserting a picture into a chart:

Insert a text box into a chart
A chart shows a trend but cannot state a conclusion. A total for one series, a year-on-year remark or a callout on an outlier can all be written straight onto the chart with a text box, sparing the reader a second trip to the body text for the number. A text box is a shape inside the chart like the picture, measured on the same scale; the difference is that its size has to be sized to the length of the text. Leave it too narrow and the text wraps, and the wrapped line is clipped by the box height — it looks as though half the words went missing. The steps are:
- Load the font and the test data file into the VFS.
- Load the workbook with
workbook.LoadFromFileand take the first chart on the first worksheet. - Create the text box inside the chart with
chart.Shapes.AddTextBox. - Set
Left,Top,WidthandHeightaccording to the length of the text so that the content fits on one line. - Write the text into the
Textproperty. - Centre the text with
HAlignmentandVAlignment, taking the values fromxlsModule.CommentHAlignTypeandxlsModule.CommentVAlignType. - Set the fill, the border colour and the border width with
Fill.ForeColor,Line.ForeColorandLine.Weight, so that the box stands out on the chart. - Save the workbook with
workbook.SaveToFile.
The complete code example below shows how to insert a text box into a chart in React:
function App() {
const addTextBoxInChart = async () => {
// get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// check that the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// load the font and the test data file into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ChartReport.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// take the first worksheet and the chart on it
const sheet = workbook.Worksheets.get(0);
const chart = sheet.Charts.get(0);
// Add a text box to the chart
const textBox = chart.Shapes.AddTextBox();
// set the position and size of the text box, again in 1/4000 of the chart
textBox.Left = 450;
textBox.Top = 530;
textBox.Width = 2100;
textBox.Height = 340;
// write the text
textBox.Text = 'Online: 1,208K USD in total';
// centre the text and give the box a pale yellow fill and a blue border so that it stands
// out on the chart
textBox.HAlignment = xlsModule.CommentHAlignType.Center;
textBox.VAlignment = xlsModule.CommentVAlignType.Center;
textBox.Fill.ForeColor = xlsModule.Color.FromArgb(255, 255, 245, 214);
textBox.Line.ForeColor = xlsModule.Color.FromArgb(255, 46, 106, 176);
textBox.Line.Weight = 1;
// save the workbook
const outputFileName = 'AddTextBoxInChart.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// dispose of the workbook to free resources
workbook.Dispose();
// read the result file from the VFS and start the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Picture and a Text Box in a Chart</h1>
<button id="add-textbox-in-chart" onClick={addTextBoxInChart}>Add a text box to the chart</button>
</div>
);
}
export default App;
After running, inserting a text box into a chart:

Fill the plot area with a picture
The white background of the plot area is the chart's default, and a report that has been around for a while starts to look the same everywhere. Replacing it with a light texture keeps the columns, the gridlines and the axis labels perfectly legible while giving the background some depth, and pulls the chart into the same visual language as the rest of the report. The fill and the picture shapes placed on the chart do not interfere with each other; the two can be used together. The steps are:
- Load the font, the test data file and the background picture into the VFS.
- Load the workbook with
workbook.LoadFromFileand take the first chart on the first worksheet. - Build an
xlsModule.Streamin memory from the background picture. - Hand it to
chart.PlotArea.Fill.CustomPicture; the second parameter,name, names an existing texture in the workbook, and'None'is passed when there is none. - Save the workbook with
workbook.SaveToFile.
The complete code example below shows how to fill the plot area with a picture in React:
function App() {
const fillPlotAreaWithPicture = async () => {
// get the Spire.XLS WASM module
const xlsModule = window.wasmModule?.spirexls;
// check that the module is ready
if (!xlsModule) {
alert('Spire.Xls is not ready yet');
return;
}
// load the font, the test data file and the background picture into the VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'ChartReport.xlsx';
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}data/`);
await window.spire.FetchFileToVFS('background.png', '', `${process.env.PUBLIC_URL}static/image/`);
// load the workbook
const workbook = new xlsModule.Workbook();
workbook.LoadFromFile({ fileName: inputFileName });
// take the first worksheet and the chart on it
const sheet = workbook.Worksheets.get(0);
const chart = sheet.Charts.get(0);
// read the background picture into a memory stream and use it to fill the plot area
const background = new xlsModule.Stream('background.png');
chart.PlotArea.Fill.CustomPicture({ im: background, name: 'None' });
// save the workbook
const outputFileName = 'FillPlotAreaWithPicture.xlsx';
workbook.SaveToFile({ fileName: outputFileName });
// dispose of the workbook to free resources
workbook.Dispose();
// read the result file from the VFS and start the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
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 Picture and a Text Box in a Chart</h1>
<button id="fill-plot-area" onClick={fillPlotAreaWithPicture}>Fill the plot area with a picture</button>
</div>
);
}
export default App;
After running, filling the plot area with a picture:

FAQ
Can I add an arrow or a callout line to a chart?
Solution: An arrow or a callout line that points at something has no API of its own, but a text box can stand in for one: stretch it into a thin strip, drop the fill and keep only the border, and place it where the callout should point.
When filling with a picture, is it the plot area or the whole chart that gets filled?
Cause: PlotArea.Fill and ChartArea.Fill are two different objects. The first covers only the region enclosed by the axes, leaving the chart title, the legend and the axis labels outside the picture. The second covers the entire chart, so the title and the legend end up on top of the picture as well.
Solution: Pick whichever one you need. After the fill, read Fill.FillType; it returns ShapeFillType.Picture on success:
// fill only the plot area: the title, the legend and the axis labels stay as they were
const background = new xlsModule.Stream('background.png');
chart.PlotArea.Fill.CustomPicture({ im: background, name: 'None' });
// fill the whole chart: the picture runs under the title and the legend
chart.ChartArea.Fill.CustomPicture({ im: new xlsModule.Stream('background.png'), name: 'None' });
Get a Free License
Spire.XLS for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.