Positioning is the most error-prone part of CSS layout, and in PDF rendering the stakes are higher: what does absolute position relative to, does fixed repeat on every page, and why does z-index sometimes do nothing? Nearly every developer has hit all three, and debugging them can eat half a day. dompdf.js supports the position system with static, relative, absolute, and fixed, and renders z-index stacking per standard CSS. The standout difference from the web is that fixed elements repeat on every page of the PDF, which is exactly the mechanism behind watermarks, headers, and corner badges that must appear throughout a multi-page document, and it is one of the features that most distinguishes document PDFs from web pages. This article walks through the four position values, explains containing blocks and offset properties, and demystifies z-index and stacking contexts with the rules that actually govern them. Complete code examples show watermarks, corner badges, and floating tags, followed by the handling of special cases and a catalog of the pitfalls that cause the most confusion, so you can control both position and stacking order precisely in your PDFs.You will also learn the practical rules that prevent the classic positioning disasters: which ancestor actually anchors an absolute element, why fixed is the only reliable choice for per-page repetition, and how stacking contexts silently override your z-index values. The examples are built for real templates, watermarks, badges, and floating tags, and the troubleshooting section is organized by symptom so you can fix a mispositioned element without re-reading the theory. By the time you finish, positioning in PDFs will feel as predictable as it does in the browser.
position has four fundamental values. static is the default: the element follows normal flow and offset properties have no effect. relative keeps the element in flow but lets top, left, and friends shift it from its original spot, which suits fine-tuning and, critically, establishes the positioning context for absolute children.
absolute takes the element out of flow and positions it relative to the nearest positioned ancestor, one whose position is not static, falling back to the page root if none exists. fixed also leaves flow but positions relative to the page viewport. In dompdf.js the decisive difference is pagination: absolute follows the content flow of its page, while fixed re-renders on every page.
Understand the consequences of leaving flow: absolute and fixed elements no longer contribute to their parent's size, so the parent can collapse. If a template needs a positioned element without disturbing layout, give the parent an explicit height, or use relative plus offsets instead, and choose the approach based on whether the element belongs to the layout or floats above it.
Choosing the right position value is mostly about answering one question: should the element move with its content or stay with the page? Content-scoped elements, such as a badge on a product card, belong in absolute, while page-scoped elements, such as a confidentiality mark, belong in fixed. Using absolute where fixed belongs produces elements that vanish after the first page, and using fixed where absolute belongs produces the same badge on every page, both easy to spot once you know the question.
Relative positioning is the quiet workhorse: it establishes the containing block for absolute children, it nudges elements a few pixels without leaving the flow, and it costs nothing in complexity. A good rule is to make every container that will host an absolute element position: relative, even if you never offset it, because retrofitting the declaration later is exactly how positioning bugs get introduced in the middle of a project.
The containing block decides where an absolute element anchors: the padding box of the nearest positioned ancestor. The classic bug is an ancestor without any position declaration, which silently makes the element position against the page root, far from where the developer intended. Adding position: relative to the ancestor fixes it instantly and is the single most common positioning fix.
The offset properties top, right, bottom, and left displace the element within its containing block and can be combined: top: 10px; left: 10px pins to the top-left corner, while left: 0; right: 0 together with margin: auto centers a fixed-width element horizontally, a classic centering trick that works identically in PDF templates.
Percentage offsets resolve against the containing block's dimensions: top: 50% is half the containing block's height, and combined with transform: translateY(-50%) it produces exact vertical centering. This combination is invaluable for corner badges and floating tooltips because it needs no concrete dimensions and stays correct across page sizes.
Offset values interact in ways that are easy to forget: top and bottom together stretch an absolutely positioned element between the two edges, which is how full-width bars inside a container are built, and left with right does the same horizontally. Add margin: auto to a stretched element with a fixed width and it centers; add it without a width and the element fills the space. These combinations replace a surprising amount of manual coordinate math in templates.
Percentage offsets resolve against the containing block's size, which makes them the responsive choice, but they also make the element's position depend on that size. If the containing block's dimensions are themselves content-driven, the positioned element can shift between pages as content varies. For stable placement in paginated documents, prefer fixed pixel offsets for chrome-like elements and percentages only when the anchor size is genuinely stable.
z-index orders positioned elements, higher values on top, and only elements with a position other than static honor it; in-flow elements sit below positioned ones. dompdf.js follows the standard stacking rules, so watermarks, badges, and overlays can be layered with confidence and match their browser appearance.
The stacking context is the key to understanding z-index failures: any positioned element with a z-index, any element with opacity below 1, and any transformed element creates its own stacking context, and z-index values inside it only compete within that context, never against elements outside it. This is why a huge z-index sometimes loses to a smaller one.
When z-index appears to fail, check whether a stacking context is the culprit: if the parent carries opacity, transform, or another context-creating property, the child's z-index is confined inside it. Remove the context-creating property from the parent, or lift the element to an outer level, and the intended order returns immediately.
A useful mental model for z-index: think of it as sorting within a family, not globally. Each stacking context is a separate family, and the browser, and dompdf.js, resolve conflicts by walking the family tree. That is why a watermark inside a transformed container can end up below an unremarkable sibling: the transform isolated the whole family. When layering misbehaves, look upward in the tree before blaming the numbers.
Disciplined z-index values keep templates maintainable: reserve a small set of tiers, such as 10 for content, 100 for floating UI, and 999 for watermarks, and document them once in the template header. Arbitrary values like 2147483647 work in the moment and become a nightmare later, because every future element must outbid them. A documented tier system turns layer management from archaeology into a lookup.
The example combines the four techniques that cover most template needs: a fixed watermark that repeats on every page, an absolute badge anchored to a relative container with a negative top offset, a corner annotation pinned with bottom and right, and the z-index tiers that keep the watermark above everything. The negative offset on the badge is worth noting, because it lets a badge straddle its container's edge, a look that tables and flow layouts cannot produce. Run it, then move the badge to different corners by swapping the offset properties.
The watermark uses percentage offsets plus a transform for centering, so it stays centered regardless of page size. That combination, position: fixed with top: 45% and translateY, is the robust way to center overlays in paginated output, because it never depends on knowing the page height in advance. Keep this pattern in your template toolbox for any overlay that must sit at a stable visual position.
import { DomPDF } from 'dompdf.js';
const html = `
<style>
.wrap { position: relative; padding: 24px; border: 1px solid #d5dbe3; }
.badge {
position: absolute;
top: -10px; right: 16px;
background-color: #e74c3c;
color: #fff;
border-radius: 4px;
padding: 2px 10px;
font-size: 12px;
z-index: 10;
}
.corner {
position: absolute;
bottom: 8px; right: 12px;
font-size: 11px;
color: #999;
}
.watermark {
position: fixed;
top: 45%; left: 0; right: 0;
text-align: center;
font-size: 44px;
color: #999;
opacity: 0.12;
transform: rotate(-45deg);
z-index: 999;
}
</style>
<div class='watermark'>Confidential - Internal</div>
<div class='wrap'>
<span class='badge'>Best Seller</span>
<h2>Wireless Noise-Cancelling Headphones</h2>
<p>Product description and specification details...</p>
<span class='corner'>SKU: P-2026-0817</span>
</div>`;
const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(html, { format: 'A4' });
pdf.save('position-demo.pdf');
Fixed elements repeat on every page in dompdf.js, the biggest behavioral difference between PDF documents and web pages, and the foundation for watermarks, page-level hints, and corner badges that must appear on every page: one declaration, and all pages automatically carry the element, with no need to insert it per page.
This behavior unlocks practical effects: a confidential watermark on every page, a version badge in the top-right corner of every page, and a disclaimer at the bottom of every page. Combined with opacity and z-index control, these elements present reliably without fighting the body text, raising both the professionalism and the security posture of the document.
Divide the work between fixed elements and @page margin boxes: fixed suits content-related overlays such as watermarks and badges, while @page margin boxes handle headers, footers, and page numbers. Both can coexist without conflict, and choosing based on the element's semantics keeps the template structure clear and maintainable.
Per-page repetition has a performance angle: every fixed element is drawn on every page, so a heavy fixed element multiplies its cost by the page count. Watermarks and badges should stay light, plain text or simple shapes, and avoid large images or complex gradients. For a thousand-page export, a heavy watermark means a thousand copies of that weight, so the template-level savings are real.
There is also a semantic rule worth following: fixed elements should carry page-independent information. The page number belongs in @page margin boxes, because it changes per page and the counter machinery handles it; the document title belongs in the header margin box for the same reason. Fixed positioning is for content that is identical on every page, and keeping that boundary clear makes the template easier to reason about.
Q: The absolute element ignores its intended ancestor? A: Check whether the ancestor has position: relative; this is the most frequent positioning bug. Verify every level of the positioning chain, especially inside nested components, and the misplacement resolves quickly.
Q: z-index is set but does nothing? A: Confirm the element is actually positioned, position other than static, and check whether a parent stacking context confines it. Remove opacity, transform, or similar context-creating properties from the parent, or restructure to move the element outward, and the order comes back.
Q: The fixed element does not repeat on every page? A: Verify it uses fixed rather than absolute. Absolute elements live only on the page containing their content and follow the flow across breaks; switching to fixed gives you the per-page repetition you expected.
Q: A positioned element gets clipped by pagination? A: Elements positioned beyond the page boundary are clipped, so check whether offsets exceed the content area. For fixed elements, prefer percentage offsets or transform-based centering over absolute pixel values, which stay safe across paper sizes.
A subtle bug class involves fixed elements and transforms: a transform on the fixed element's ancestor can change its containing block, causing the element to position relative to that ancestor instead of the page. If a fixed element drifts unexpectedly, check for transforms anywhere up the ancestor chain and remove them or restructure. The same applies to opacity values below 1, which create stacking contexts that can swallow z-index.
Debugging positioning benefits from a visualization step: temporarily give the positioned element a bright border and a translucent background, then export and look at where it actually landed. This usually reveals the anchor, the offsets, and the stacking in one glance, far faster than reading declarations. Once the element sits correctly, remove the debug styles and the template is clean again, and you have learned which ancestor was really anchoring it.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。