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 & valueEffect
break-before: pageForce a page break before the element
break-after: pageForce a page break after the element
break-inside: avoidKeep the element on a single page
page-break-before: alwaysLegacy alias for break-before: page
page-break-after: alwaysLegacy 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.

ℹ️
Combining this with a footer (e.g. page numbers or a branding bar below the pinned box)? Put the footer in an @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.