Default page
Without any @page rule, EggPdf renders on A4 portrait with
no margins. Content starts at the top-left corner of the page. Use @page to
set the size and add whitespace:
@page {
size: A4 portrait;
margin: 2cm;
}See the Configuration page for the full list of named sizes, orientation keywords, and custom dimensions.
Mixed orientations
Named pages let individual elements use a different page size or orientation. Define a
named @page rule and assign it via the CSS page property:
/* Default: portrait */
@page { size: A4 portrait; margin: 2cm; }
/* Named page: landscape */
@page wide { size: A4 landscape; margin: 1.5cm; }
/* Assign to elements */
.chart { page: wide; }
Elements with page: wide will render on their own landscape pages; all
other content uses the default portrait layout. Each named page gets its own
size, margin and margin boxes, content reflows to that page's
width, and counter(page)/counter(pages) keep counting across
groups.
An element may declare page: at any depth: the wrappers around it are split
across the page groups (each fragment keeps the wrapper's attributes and styling).
Limitations: @page name:first is ignored, CSS counters restart in each page
group, sibling selectors such as :nth-child only see the elements of their own
group (and a fragmented wrapper's borders/backgrounds repeat on each fragment), and
internal #anchor links cannot cross groups.
Page breaks
Use CSS break properties to control where EggPdf starts new pages. Both the modern
break-* properties and the legacy page-break-* aliases are
supported:
| Property & value | Effect |
|---|---|
break-before: page | Force a page break before the element |
break-after: page | Force a page break after the element |
break-inside: avoid | Keep the element on a single page |
page-break-before: always | Legacy alias for break-before: page |
page-break-after: always | Legacy alias for break-after: page |
/* Start each chapter on a new page */
.chapter { break-before: page; }
/* Keep a figure together with its caption */
.figure { break-inside: avoid; }
/* Don't split table rows */
tr { break-inside: avoid; }Pinning content to the bottom of the last page
A common pattern — a signature block, an acceptance/consent box, a summary total — needs
to sit flush against the bottom of whichever page it ends up on, even when everything
above it is dynamic, variable-length content that auto-paginates across an unknown number
of pages. Standard CSS has no way to express this: flexbox's margin-top: auto
bottom-pinning trick only works when the flex container is guaranteed to be a single
physical page, which dynamic content can't guarantee up front.
EggPdf's -eggpdf-pin-bottom: page extension solves this directly. Apply it to
the element that should hug the bottom edge — once pagination has run, if that element
(and anything after it in the flow) fits within a single physical page, it's shifted down
to the bottom of that page:
.acceptance-box {
-eggpdf-pin-bottom: page;
}If the marked element itself is too tall to fit on one physical page, it's left exactly where normal flow placed it rather than forced — there's no single "page bottom" to align to in that case, so pinning it would corrupt the layout instead of fixing it.
@page margin box such as @bottom-center and
reserve room for it with @page margin-bottom — not body margin or a
wrapper's padding, which don't reserve space per physical page. A
position: fixed footer is placed inside the page area, like in Chrome. See
Headers & Footers for that pattern.
Flexbox and Grid
Both display: flex and display: grid work exactly as in a
browser. EggPdf uses the same CSS box model, so multi-column layouts flow naturally
within the page's content area.
/* Two equal columns with a gap */
.two-col {
display: flex;
gap: 24px;
}
/* Three equal columns */
.three-col {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
Flex and grid items flow within the page's content area. If the combined height exceeds
the available space, the layout continues on the next page. Apply
break-inside: avoid to individual items to prevent them from being split
across pages.
grid-auto-flow: row dense / column dense backfills earlier
gaps left by items that couldn't fit the current row/column, matching a browser instead
of always placing items forward from the last cursor position.
grid-auto-rows / grid-auto-columns size the implicit tracks
the auto-placement algorithm creates beyond an explicit
grid-template-rows/grid-template-columns (a value list cycles
across successive implicit tracks).
Floats and shape-outside
float: left / float: right narrow sibling inline content per
line, the same as a browser — a paragraph next to a floated image indents around it,
not just past its top edge. shape-outside: circle(),
ellipse(), polygon() and inset() refine the exclusion to the shape's actual per-line extent instead
of the float's full rectangular bounding box (rounded inset() corners included);
url() shapes follow the image's alpha above shape-image-threshold
(a JPEG has no alpha, so it keeps the float's plain rectangular exclusion). Floated
<img> elements wrap text the same way. clear: left / right /
both pushes content below active floats as expected.
/* Text wraps around the image, narrowing near its top/bottom via shape-outside */
.pull-image {
float: left;
width: 150px;
shape-outside: circle(50%);
margin: 0 16px 8px 0;
}overflow: hidden and contain: paint
overflow: hidden / overflow: clip and
contain: paint / contain: strict clip an element's painted
content — including descendants painted separately from the element itself —
to its bounds, not just its own background and border. contain's other
values (layout, style, content alone) are
recalculation-isolation hints with no analog in a single-pass renderer and are accepted
as a no-op.