Pseudo-elements are the most elegant decoration tool in CSS. The quotes before a paragraph, the numbers on a list, the divider under a heading, the currency symbol in front of a price — all of it can be rendered with ::before and ::after plus the content property, without cluttering the HTML with extra tags. When a page exports to PDF, pseudo-element support decides how templates are written: a library that supports them lets pure CSS carry the decoration, while one that does not forces hand-written tags and a more complex template. dompdf.js supports ::before and ::after with the content property, including strings, attr(), and counters, so decoration that works in the browser renders identically in the PDF. This guide covers the value of pseudo-elements in document templates, the basics of ::before and ::after, a complete code example combining numbering, quotes, and dividers, the full range of content values including nested counters, and the support boundaries you should design around. The closing section answers the questions that appear in real projects. The payoff is cleaner templates: decoration separated from data, numbering maintained automatically, and DOM structure that stays semantic — the difference between templates that fight you and templates that work. The sections progress from motivation to mechanics: why pseudo-elements earn their place in PDF templates, the basics of ::before and ::after, a complete example combining the three most common decorations, the full range of content values including nested counters, and the support boundaries that should shape how you write them. Code appears throughout, and the closing section answers the questions that surface in real projects, including the extraction and escaping pitfalls that catch teams late.
Pseudo-elements free decoration from extra tags: the ornament before a paragraph, the divider beside a heading, the numbered dot on a list item, the quotation mark on a blockquote — all rendered by CSS while the HTML stays semantically clean. For PDF templates, cleaner structure means easier maintenance: decoration lives in one place, changes touch styles only, and the markup keeps its meaning. That separation is the core value, and it compounds as templates grow.
In PDF generation there is a second reason pseudo-elements matter: templates are usually assembled dynamically from data, and inserting decorative tags around every data item is awkward and error-prone. A pseudo-element attached to a fixed class survives any data change, so a contract clause number or a report section marker rendered by pseudo-elements stays present no matter what the data contains. Template code and data logic stay decoupled, and new items get their decoration automatically.
Pseudo-elements also carry a semantic role: decorative content placed in pseudo-elements does not enter the reader's content flow, so the document's semantics stay clean. But when the content holds real information — a number, a price, a label — the fact that pseudo-element content is not part of the text layer changes the decision. Information belongs in real elements; decoration belongs in pseudo-elements; the boundary is the design rule that keeps both uses working.
For accessibility and search, the same boundary applies: if the PDF's text must be extractable, searchable, or copyable, any content that matters should be real text. Pseudo-element content is rendered visually but not guaranteed in extraction, so treating the visual layer and the information layer as distinct is not pedantry — it is the difference between a document that works and one that only looks like it works.
::before and ::after generate an inline box before and after an element's content, and both require the content property — even content: "" is legal and commonly used to create purely decorative boxes such as dividers. Pseudo-elements are inline by default; giving them width or height requires switching display to block or inline-block, and properties like display and position work on them without restriction, so their layout capabilities match real elements.
A pseudo-element inherits inheritable properties — font, color, and so on — from the element it is attached to, and its content length is unrestricted. What it does not do is participate in the parent's text semantics: PDF text extraction may not include pseudo-element content. That single limitation defines the boundary between decorative use, which is safe, and informational use, which needs real elements. Design with that boundary in mind and both use cases stay reliable.
Pseudo-elements carry their own styles independently: font size, weight, color, and background can all differ from the parent, separating decoration from body text. A section heading's number can render in the brand color at a larger size while the heading text stays default — one class name completes the whole treatment, template code shrinks, and style changes concentrate in the pseudo-element rules, where a single edit updates the entire document.
Pseudo-elements can also be positioned: absolute positioning relative to a positioned parent works normally, which makes badges, corner marks, and overlaid labels possible in the template without extra markup. The positioning model is the same as real elements, so anything you can position in CSS you can position in a pseudo-element. The main constraint is inheritance: pseudo-elements pick up inherited values from their host element, so a pseudo-element that must look different needs its own explicit declarations rather than relying on inherited ones.
Quote decoration writes the characters directly in content, watching the escaping of quote characters inside template strings and choosing Chinese or English quotation marks by document language. A currency symbol in a pseudo-element keeps the data field purely numeric: the template and the data stay separate, switching currencies changes one style rule and the whole document updates, which is far cleaner than concatenating symbols into strings at the data layer and avoids coupling data format with display format.
Counters are the high-end use of pseudo-elements: counter-reset initializes on the parent container, counter-increment increments on each target element, and content: counter(section) outputs the current value. Chapter numbers, clause numbers, and figure numbers all generate automatically; inserting or deleting an item renumbers everything with no manual maintenance, and multi-page documents never show duplicated or skipped numbers, because the numbering derives from document structure rather than hand-edited text.
Divider decoration uses empty content plus display: block and a background color, which is more flexible than borders: length, position, and gradient are all controllable, and margin plus width produce short rules, centered dashes, and other common ornaments. The divider under a heading is the most frequent decoration in document-class PDFs, and one rule completes it, keeping the template minimal and the output professional.
Because counters and decoration are purely structural, they survive template refactoring: moving a section, promoting it to a higher level, or converting a list to a different element type renumbers automatically. The style rules stay attached to class names, so the decoration follows the content wherever the content goes.
<style>
.quote::before { content: '“'; font-size: 2em; color: #888; }
.quote::after { content: '”'; font-size: 2em; color: #888; }
.divider::after { content: ''; display: block; height: 2px; background: #333; margin-top: 8px; }
.price::before { content: '¥'; font-weight: bold; }
.doc { counter-reset: section; }
.section-num { counter-increment: section; }
.section-num::before { content: counter(section) '. '; color: #1a56db; font-weight: bold; }
.required::after { content: ' *'; color: red; }
</style>
<div class="doc">
<h2 class="section-num">Chapter 1: Project Background</h2>
<p class="quote">Quoted text gets Chinese quotation marks added automatically.</p>
<p class="price">1,299.00</p>
<p>Required field <span class="required">Company name</span></p>
<div class="divider"></div>
</div>
content accepts strings, attr(), counter() and counters(), quotes (open-quote and close-quote), and images via url(), and the values can be combined. Strings are the workhorse; attr(href) outputs an element's attribute value, which prints link URLs in the PDF — useful for documents that must show the addresses behind links; open-quote pairs quotes automatically when combined with the quote style definitions.
counters() handles nested numbering: multi-level list numbers like 1.1 and 1.2.1 output with counters(section, '.'), the hierarchy joined automatically. Multi-level clause and heading numbering systems collapse into a single rule, and because the numbers derive from nesting, restructuring the document renumbers everything correctly without touching the data layer. The numbering always matches the structure because they are the same thing.
Images are legal in content via url(), suited to icon decoration — but images load asynchronously, so generation must wait for the resource to be ready, following the same discipline as fonts. Decorative images prefer SVG or small bitmaps embedded in the template or served as static assets, balancing size and sharpness; solid-color icons render more cheaply as characters or CSS shapes, which keeps performance and visual quality both high.
Combining values is where content gets expressive: a counter followed by a string, an attr value wrapped in quotes, an open-quote before dynamic text. Each combination is still one declaration on one class, so the expressive power costs nothing in template complexity — the decoration stays centralized even when it is elaborate.
A common pattern worth knowing is the combination of a string and a counter for labels like 'Section 3 — Overview': content: 'Section ' counter(section) ' — ' places the fixed text around the generated number in a single declaration. The same technique composes with attr() for dynamic values, so a template can generate complete, information-bearing labels while keeping the data layer minimal and the structure semantic.
dompdf.js supports ::before and ::after with the common content values, but no library porting browser CSS can cover every edge of the platform. Template development should avoid depending on bleeding-edge constructs — exotic attr() expressions, extreme nesting scenarios, or styles that depend on runtime state — because they may render fine in the browser and fail silently in the PDF. Designing to the supported baseline keeps templates portable and predictable.
The most important limitation is the text layer: pseudo-element content does not participate in PDF text extraction and search. If the document must be searchable, copyable, or machine-processed, informational content like numbers and labels belongs in real elements; only decorative content belongs in pseudo-elements. Following the information-in-elements, decoration-in-pseudo-elements rule satisfies both needs without compromise.
Other limitations are minor in PDF context: pseudo-elements cannot carry events or interaction, which does not matter in a static document, and advanced selector combinations like :hover variants have no meaning in a static PDF, so they are not worth engineering effort. When rendering disagrees with the browser, run a minimal test — isolate the pseudo-element rule on a single element and compare — which distinguishes a support gap from a writing mistake quickly, and far more efficiently than reworking the template blindly.
Because the supported baseline is narrower than the browser's, keeping decoration on the simple side is not a compromise but a design principle: the simplest content values — strings and counters — are exactly the ones that work everywhere, so templates built on them are the most portable assets your team produces. When a new feature request arrives, try the basic form first; in the large majority of cases it renders identically and saves the whole debugging cycle.
Q: Pseudo-elements do not show in the PDF. A: Check that the content property exists — an empty string counts — confirm the selector is correct and not overridden, verify special characters in content are properly escaped, and render the same template in the browser for comparison. If the browser renders it and the PDF does not, the construct is likely outside the supported range; fall back to a more basic form.
Q: Counter numbers do not increment. A: Verify the mounting points: counter-reset on the parent container, counter-increment on the target elements, and the increment rule's selector actually matching those elements. Independent numbering systems use separate counter names so they cannot interfere, and with those three positions correct, numbering is stable and predictable.
Q: Content with quotes or special characters breaks the template. A: Escape the characters at the point of writing — inside template literals and inside the JSON or build step that carries the template. The quote characters themselves need escaping in the same way strings always do, and checking the escaped form in the browser before export catches the mismatch.
Best-practice summary: decoration in pseudo-elements, information in real elements; strings and counters for content; escape special characters; verify in the browser before generation; degrade elaborate decoration to basic forms. Following these five practices, pseudo-elements decorate PDF templates reliably instead of becoming a debugging sink — clean templates, stable output, and a codebase the whole team can extend without fear.
Q: Pseudo-element content disappears when the PDF text is extracted or searched. A: That is expected: pseudo-element content is visual, not part of the text layer. If the information must be extractable — clause numbers, prices, required markers — move it into real elements and keep pseudo-elements for pure decoration. Decide per use case at template design time rather than discovering the gap when a customer tries to search the delivered PDF.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。