Microsoft Word's built-in watermark feature can place only one watermark at a time, and its position and number are fixed by the built-in layout, so it cannot tile the whole page with multiple rows and columns. For contracts, internal documents and presentation drafts that need full-page coverage, the watermark can be implemented by adding a shape or a picture to the header and then cloning it by row and column.
This article adds multi-row, multi-column text watermarks and image watermarks to Word documents in the browser with Spire.Doc for JavaScript. The whole process runs in the browser on WebAssembly, requires no Office installation and depends on no backend service, and the file always remains on the user's device.
This article covers the following topics:
- Add Multi-Row Text Watermarks
- Add Multi-Row Image Watermarks
- Adjust the Tiling Rows, Columns and Spacing
The two kinds of watermarks are implemented as follows:
| Watermark type | Carrier object | How it is created | Key properties |
|---|---|---|---|
| Text watermark | WordArt shape | new ShapeObject(doc, ShapeType.TextPlainText) |
WordArt.Text, WordArt.FontFamily, Rotation, FillColor |
| Image watermark | Picture object | new DocPicture(doc) with LoadImage() |
TextWrappingStyle, Width, Height |
For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume that Spire.Doc is installed and the WebAssembly module is initialized. The examples read Sample.docx and logo.png from the project's public/data directory and the font file from the public/font directory, so put the corresponding files into these two directories before running them.
Add Multi-Row Text Watermarks
To add a multi-row, multi-column text watermark, a WordArt shape is created in the header, then cloned in a loop over rows and columns, and each copy is given its own vertical and horizontal coordinates. The watermark object lives in the header, so it is repeated on every page together with the header and does not have to be added page by page.
The following members are involved in adding multi-row text watermarks:
| Member | Purpose |
|---|---|
ShapeObject |
Shape object that carries the watermark text |
ShapeType.TextPlainText |
Sets the shape to ordinary WordArt |
ShapeObject.Width, Height |
Width and height of the shape, in points |
ShapeObject.VerticalPosition, HorizontalPosition |
Vertical and horizontal coordinates of the shape on the page |
ShapeObject.Rotation |
Rotation angle, in degrees |
WordArt.Text, WordArt.FontFamily |
WordArt content and font |
ShapeObject.FillColor, StrokeColor |
Text fill color and outline color |
ShapeObject.Clone() |
Clones the shape for tiling |
HeaderFooter.AddParagraph() |
Adds a paragraph in the header to carry the watermark |
Paragraph.ChildObjects.Add() |
Adds the shape to the header paragraph |
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the sample document and the font into the virtual file system (VFS), create a
Documentobject and load the document. - Create a
ShapeObject, set its shape type toShapeType.TextPlainText, and set its width, height, rotation angle, WordArt content, font, fill color and outline color. - Iterate over every section of the document, get the header through
HeadersFooters.Header, and add a paragraph withAddParagraph(). - Clone the shape with
Clone()in a loop over rows and columns, give each copy its ownVerticalPositionandHorizontalPosition, and add it to theChildObjectsof the header paragraph. - Save the result file with
SaveToFile(), read it from the VFS and trigger a browser download.
The code for adding multi-row text watermarks is as follows:
function App() {
const addTextWatermark = async () => {
// Get the Spire.Doc WASM module
const wasmModule = window.wasmModule?.spiredoc;
if (!wasmModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample document and the font
await window.spire.FetchFileToVFS('Sample.docx', '', `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
await window.spire.FetchFileToVFS('arialbd.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// Create a Document instance and load the document
const doc = new wasmModule.Document();
doc.LoadFromFile('Sample.docx');
// Create a WordArt shape and set its size, position, rotation and style
let shape = new wasmModule.ShapeObject(doc, wasmModule.ShapeType.TextPlainText);
shape.Width = 60;
shape.Height = 15;
shape.VerticalPosition = 25;
shape.HorizontalPosition = 20;
shape.Rotation = 320;
shape.WordArt.Text = 'INTERNAL USE';
shape.WordArt.FontFamily = 'Arial';
shape.FillColor = wasmModule.Color.FromArgb(255, 255, 0, 0);
shape.StrokeColor = wasmModule.Color.FromArgb(255, 255, 0, 0);
// Iterate over all sections and add the watermark to the header of each section
for (let n = 0; n < doc.Sections.Count; n++) {
const section = doc.Sections.get_Item(n);
const header = section.HeadersFooters.Header;
const paragraph = header.AddParagraph();
// 5 rows and 6 columns: clone the shape and set the coordinates of each copy
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 6; j++) {
shape = shape.Clone();
shape.VerticalPosition = 50 + 150 * i;
shape.HorizontalPosition = 20 + 85 * j;
paragraph.ChildObjects.Add(shape);
}
}
}
// Save the result file
const outputFileName = 'AddTextWatermark.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
doc.Dispose();
// 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/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Add Multi-Row Text Watermarks</h1>
<button onClick={addTextWatermark}>Generate Document</button>
</div>
);
}
export default App;
After adding multi-row, multi-column text watermarks, the document looks like this:

Add Multi-Row Image Watermarks
To add a multi-row, multi-column image watermark, a picture is loaded into the header, then cloned by row and column and given its own coordinates. Compared with a text watermark, the appearance of an image watermark is determined by the picture itself, and the code only has to control the wrapping style and the arrangement of the copies.
The following members are involved in adding multi-row image watermarks:
| Member | Purpose |
|---|---|
DocPicture |
Object that carries the watermark image |
DocPicture.LoadImage(path) |
Loads the watermark image from the VFS |
TextWrappingStyle.Behind |
Places the picture behind the body text |
DocPicture.VerticalPosition, HorizontalPosition |
Vertical and horizontal coordinates of each copy |
DocPicture.Clone() |
Clones the picture object for tiling |
HeaderFooter.AddParagraph() |
Adds a paragraph in the header to carry the watermark |
Paragraph.ChildObjects.Add() |
Adds the picture to the header paragraph |
The specific steps are as follows:
- Get the Spire.Doc WASM module and confirm that it is ready.
- Load the sample document and the watermark image into the virtual file system (VFS), create a
Documentobject and load the document. - Iterate over every section of the document, get the header through
HeadersFooters.Header, and add a paragraph withAddParagraph(). - Create a
DocPicture, callLoadImage()to load the watermark image, and setTextWrappingStyletoTextWrappingStyle.Behind. - Clone the picture with
Clone()in a loop over rows and columns, give each copy its ownVerticalPositionandHorizontalPosition, and add it to theChildObjectsof the header paragraph. - Save the result file with
SaveToFile(), read it from the VFS and trigger a browser download.
The code for adding multi-row image watermarks is as follows:
function App() {
const addImageWatermark = async () => {
// Get the Spire.Doc WASM module
const wasmModule = window.wasmModule?.spiredoc;
if (!wasmModule) {
alert('Spire.Doc is not ready yet');
return;
}
// Load the sample document and the watermark image
await window.spire.FetchFileToVFS('Sample.docx', '', `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS('logo.png', '', `${process.env.PUBLIC_URL}/data/`);
// Create a Document instance and load the document
const doc = new wasmModule.Document();
doc.LoadFromFile('Sample.docx');
// Iterate over all sections and add the watermark to the header of each section
for (let n = 0; n < doc.Sections.Count; n++) {
const section = doc.Sections.get_Item(n);
const header = section.HeadersFooters.Header;
const paragraph = header.AddParagraph();
// Create a picture object and load the watermark image
const picture = new wasmModule.DocPicture(doc);
picture.LoadImage('logo.png');
picture.TextWrappingStyle = wasmModule.TextWrappingStyle.Behind;
// 5 rows and 3 columns: clone the picture and set the coordinates of each copy
for (let p = 0; p < 5; p++) {
for (let q = 0; q < 3; q++) {
const pic = picture.Clone();
pic.VerticalPosition = 50 + 150 * p;
pic.HorizontalPosition = 10 + 150 * q;
paragraph.ChildObjects.Add(pic);
}
}
}
// Save the result file
const outputFileName = 'AddImageWatermark.docx';
doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Docx2013 });
doc.Dispose();
// 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/vnd.openxmlformats-officedocument.wordprocessingml.document' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Add Multi-Row Image Watermarks</h1>
<button onClick={addImageWatermark}>Generate Document</button>
</div>
);
}
export default App;
After adding multi-row, multi-column image watermarks, the document looks like this:

Adjust the Tiling Rows, Columns and Spacing
Adjusting the tiling rows, columns and spacing means changing the loop counts and the two coefficients in the coordinate formula: the number of rows and columns determines how many watermarks there are, while the row gap and column gap determine the distance between neighboring watermarks. Together they decide how dense the tiling is on the page.
The coordinate formula used for tiling is as follows:
| Parameter | Example value | Purpose |
|---|---|---|
Rows rows |
4 | Number of watermarks arranged vertically |
Columns cols |
5 | Number of watermarks arranged horizontally |
Row gap rowGap |
180 | Vertical distance between two neighboring rows, in points |
Column gap colGap |
100 | Horizontal distance between two neighboring columns, in points |
| Start coordinates | 40, 20 | Vertical and horizontal coordinates of the first row and first column |
The coordinates of each watermark are calculated as follows:
VerticalPosition = start vertical coordinate + row gap × row indexHorizontalPosition = start horizontal coordinate + column gap × column index
The specific steps are as follows:
- Estimate the width and height available for the watermarks from the page size and the page margins.
- Decide the number of rows and columns, and the start coordinates of the first row and first column.
- Change the loop counts and the row gap and column gap in the coordinate formula, and adjust the width and height of the shape as needed.
- Save the file and preview it, then fine-tune the spacing according to the actual result.
The code for adjusting the tiling rows, columns and spacing is as follows. The example uses 4 rows and 5 columns, a row gap of 180 points and a column gap of 100 points, and changes the watermark to dark gray:
// Number of rows, columns and the spacing
const rows = 4;
const cols = 5;
const rowGap = 180;
const colGap = 100;
// Create the base shape and clone it later with Clone()
const base = new wasmModule.ShapeObject(doc, wasmModule.ShapeType.TextPlainText);
base.Width = 80;
base.Height = 20;
base.Rotation = 315;
base.WordArt.Text = 'CONFIDENTIAL';
base.WordArt.FontFamily = 'Arial';
base.FillColor = wasmModule.Color.FromArgb(255, 70, 70, 70);
base.StrokeColor = wasmModule.Color.FromArgb(255, 70, 70, 70);
for (let n = 0; n < doc.Sections.Count; n++) {
const header = doc.Sections.get_Item(n).HeadersFooters.Header;
const paragraph = header.AddParagraph();
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const shape = base.Clone();
shape.VerticalPosition = 40 + rowGap * i;
shape.HorizontalPosition = 20 + colGap * j;
paragraph.ChildObjects.Add(shape);
}
}
}
After adjusting the rows, columns and spacing, the text watermark looks like this:

FAQ
Why does the watermark not appear on the first page
Cause: Headers are set per section. The example writes to HeadersFooters.Header, which applies to the normal pages of the section. When a document uses "Different first page", the first page uses HeadersFooters.FirstPageHeader instead; when it uses "Different odd and even pages", even pages use HeadersFooters.EvenHeader. The sample document uses neither option, so the watermark appears on every page.
Solution: Add the watermark to HeadersFooters.FirstPageHeader or HeadersFooters.EvenHeader as well when the corresponding option is enabled.
Why do the rightmost columns fall outside the page
Cause: HorizontalPosition is measured in points and starts from the left page margin. With text watermarks 60 points wide, a column gap of 85 points and 6 columns, the right edge of the last column is about 20 + 85 × 5 + 60 = 505 points, which is already close to the right edge of the page once the left margin is added. If the column gap or the number of columns is increased further, the rightmost objects go beyond the page.
Solution: Before adjusting, estimate the total width as "start coordinate + gap × (count − 1) + object width" and compare it with the page width.
Why does the set position differ from the actual position on the page
Cause: Shapes in the header are positioned absolutely: horizontally from the left page margin and vertically from the top of the header area. The sample document has a left margin of 90 points and the header sits 36 points from the top of the page, so with HorizontalPosition = 20 the watermark actually appears at about 110 points from the left edge of the page.
Solution: After changing the template or adjusting the page margins, re-estimate the start coordinates.
Do duplicated image watermarks significantly increase the file size
Usually not. The copies share the same image: Clone() duplicates the picture object and the layout properties, while the image itself is stored only once in the document. The example produces 15 image watermarks, and the word/media/ directory of the docx still contains a single image file, which the other copies reference. The increase in file size comes mainly from the shape and positioning data rather than from the image data.
What if the image watermark covers the body text
The example places the picture behind the body text with TextWrappingStyle.Behind, so the pictures have a negative z-order in the generated document and the body text stays on top. If the watermark is still too prominent, reduce its opacity or use a lighter version when preparing the image; the example uses a semi-transparent, light-colored PNG.
Get a Free License
Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.
