Watermarks, Headers, and Footers: Advanced PDF Customization Techniques
The content of your PDF is only part of the story. Professional documents need polishing: a repeating header with your logo, a footer with page numbers and legal text, a "DRAFT" watermark that screams "don't share this yet." These elements seem simple, but implementing them correctly — especially across multi-page documents — is surprisingly tricky.
This guide covers the most common PDF chrome elements: what they are, when to use them, and how to implement them with various tools.
Watermarks
A watermark is text or an image overlaid on the page content, typically diagonal and semi-transparent. Common uses:
- DRAFT: Indicates the document isn't final
- CONFIDENTIAL: Warns the reader about sharing restrictions
- COPY: Distinguishes copies from originals
- Company logo: Subtle branding across every page
CSS-Based Watermarks (HTML-to-PDF)
For HTML-to-PDF conversion, a CSS watermark using a fixed-position element works well:
.watermark {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-45deg);
font-size: 80pt;
font-weight: 900;
color: rgba(0, 0, 0, 0.06);
text-transform: uppercase;
letter-spacing: 15pt;
pointer-events: none;
z-index: 1000;
white-space: nowrap;
}
<div class="watermark">DRAFT</div>
<div class="content">
<!-- Your document content -->
</div>
Important: position: fixed means the watermark appears once, anchored to the viewport. In Chromium/Puppeteer, this means it appears on every page. In WeasyPrint, behavior may differ — test it.
Image Watermarks
For a logo watermark, use a semi-transparent image:
.watermark-logo {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0.04;
width: 300px;
height: auto;
pointer-events: none;
}
Low-Level Library Watermarks (TCPDF/FPDF)
Low-level libraries give you more control. You can draw the watermark directly on each page:
class WatermarkedPdf extends TCPDF
{
private string $watermarkText = '';
public function setWatermark(string $text): void
{
$this->watermarkText = $text;
}
// This method is called automatically for each page
public function Header(): void
{
if ($this->watermarkText === '') {
return;
}
// Save current state
$this->SetAlpha(0.08);
$this->SetFont('helvetica', 'B', 60);
$this->SetTextColor(0, 0, 0);
// Rotate and position the text
$this->StartTransform();
$this->Rotate(45, 105, 148);
$this->Text(30, 180, $this->watermarkText);
$this->StopTransform();
// Restore state
$this->SetAlpha(1);
}
}
$pdf = new WatermarkedPdf();
$pdf->setWatermark('CONFIDENTIAL');
$pdf->AddPage();
$pdf->SetFont('helvetica', '', 11);
$pdf->Write(0, 'This is the document content...');
$pdf->Output('secure-doc.pdf', 'F');
Post-Processing Watermarks
If you've already generated the PDF and need to add a watermark after the fact (common in approval workflows):
# Using pymupdf to add a watermark to an existing PDF
import fitz
doc = fitz.open("original.pdf")
for page in doc:
# Create a transparent text watermark
text = "APPROVED"
point = fitz.Point(page.rect.width / 2, page.rect.height / 2)
page.insert_text(
point,
text,
fontsize=60,
rotate=45,
color=(0, 0.5, 0), # Green
overlay=True,
render_mode=0,
opacity=0.15,
)
doc.save("watermarked.pdf")
Headers and Footers
Understanding the Two Approaches
There are fundamentally two ways to add repeating headers and footers:
Method 1: CSS Paged Media (Margin Boxes)
The W3C standard way. Define content in page margin boxes:
@page {
margin: 25mm 15mm 25mm 15mm;
@top-left {
content: url('/logo-small.png');
vertical-align: middle;
}
@top-right {
content: "Acme Corp — Confidential";
font-size: 8pt;
color: #999;
vertical-align: middle;
}
@bottom-left {
content: "Generated " string(generated-date);
font-size: 7pt;
color: #999;
}
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 8pt;
color: #666;
}
@bottom-right {
content: "© 2026 Acme Corp";
font-size: 7pt;
color: #999;
}
}
Pros: Clean separation between header/footer and content, proper pagination Cons: Only works in WeasyPrint and Prince, NOT in Chromium
Method 2: Fixed-Position Elements
Use position: fixed elements that repeat on every page:
.page-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 15mm;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15mm;
border-bottom: 0.5pt solid #e5e5e5;
font-size: 8pt;
color: #999;
}
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 10mm;
display: flex;
justify-content: center;
align-items: center;
font-size: 8pt;
color: #999;
}
/* Add body padding so content doesn't overlap */
body {
padding-top: 20mm;
padding-bottom: 15mm;
}
Pros: Works in Chromium/Puppeteer Cons: No page numbers (Chromium doesn't expose page counter in CSS), content overlap risks
Method 3: Chromium Built-In Headers/Footers
Puppeteer and Playwright have a built-in header/footer option:
await page.pdf({
format: 'A4',
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 8px; width: 100%; text-align: center; color: #999;">
<span>Acme Corp — Confidential</span>
</div>
`,
footerTemplate: `
<div style="font-size: 8px; width: 100%; text-align: center; color: #999;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
`,
margin: { top: '25mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
The magic classes: pageNumber, totalPages, date, title, url — Chromium substitutes these automatically.
Pros: Real page numbers, works in Puppeteer/Playwright Cons: Very limited styling (inline only, tiny subset of CSS), separate context from main page
Dynamic Headers
For reports and longer documents, you might want the header to reflect the current section:
/* WeasyPrint/Prince — dynamic running header */
h1 {
string-set: current-chapter content();
}
h2 {
string-set: current-section content();
}
@page {
@top-left {
content: string(current-chapter);
font-size: 8pt;
}
@top-right {
content: string(current-section);
font-size: 8pt;
font-style: italic;
}
}
This automatically updates the header as the reader progresses through the document.
Branded Overlays
Some companies want more than a simple header — they want a full branded overlay: a colored sidebar, a background pattern, or a decorative border.
Sidebar Accent
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 5mm;
height: 100%;
background: linear-gradient(180deg, #2563eb 0%, #7c3aed 100%);
}
Decorative Corner Elements
.corner-decoration {
position: fixed;
width: 30mm;
height: 30mm;
}
.corner-top-right {
top: 0;
right: 0;
border-top: 3pt solid #2563eb;
border-right: 3pt solid #2563eb;
}
.corner-bottom-left {
bottom: 0;
left: 0;
border-bottom: 3pt solid #2563eb;
border-left: 3pt solid #2563eb;
}
Background Patterns
body {
background-image: url('/subtle-pattern.svg');
background-repeat: repeat;
background-size: 20mm;
background-opacity: 0.03;
}
Note: Use print-color-adjust: exact; (or -webkit-print-color-adjust: exact;) to ensure backgrounds print. Most browsers strip background colors/images by default when printing.
Best Practices
1. Content Should Not Overlap Chrome
The most common bug: document content overlapping the header or footer. Always account for header/footer height in your page margins or body padding.
2. Test with Multi-Page Documents
A header that works on one page might break on page three. Always test with enough content to generate 3+ pages.
3. Keep Headers/Footers Small
Headers should be 10-15mm maximum. Footers should be 8-12mm. Larger headers eat into your usable content area and make documents feel cramped.
4. Use Consistent Typography
Header/footer text should be noticeably smaller than body text (7-9pt vs 10-11pt body text) and in a neutral color (gray, not black).
5. First Page Can Be Different
Most professional documents have a different first page — no header, or a larger header with full company details. Use @page :first or conditional rendering.
Conclusion
Headers, footers, and watermarks are finishing touches that transform a document from "generated output" into a professional piece. The implementation approach depends on your PDF engine:
- WeasyPrint/Prince: Use CSS Paged Media margin boxes — it's the cleanest, most powerful approach
- Puppeteer/Playwright: Use the built-in
headerTemplate/footerTemplatefor page numbers,position: fixedfor visual elements - Low-level libraries: Override the
Header()andFooter()methods - API services: Configure headers, footers, and watermarks in your template settings — PDF-API.io lets you design these elements visually in its template editor
Pick the approach that matches your PDF engine, and test thoroughly with multi-page documents.
Design beautiful headers, footers, and watermarks with zero code. PDF-API.io's visual editor handles the styling — you just provide the data. Start for free.