Repository navigation
Content taller than one page is squashed/overlapped at the bottom instead of flowing onto the next page #3449
Description
Content overlaps itself at a page break instead of moving to the next page
Describe the bug
When a block lands near the bottom of a page with just barely not enough room to fit, react-pdf force-fits it into the little space left instead of moving it to the next page. Its lines are then drawn on top of each other ("overlapping / scrunched at the page break"), and the rest of the page below is wasted.
It shows up most with two-column rows and wrap={false} blocks near a page boundary (headers, footers, itinerary-style entries, table rows).
The pagination logic lives in
@react-pdf/layout, which@react-pdf/rendererdepends on. The renderer version can look unrelated (e.g. renderer4.5.1→ layout4.6.1); the bug is in layout4.6.1and is present on the latest published version.
How to reproduce
Paste into the react-pdf REPL and look at the bottom of page 1. (A plain long <Text> is not enough to trigger it — it paginates cleanly. The trigger is a wrap={false} block that is the sole child of its container and lands at the bottom of an already-populated page.)
const Block = ({ label, value }) => (
<View style={{ marginTop: 5 }} wrap={false}>
<Text style={{ fontSize: 8, fontWeight: 700 }}>{label}</Text>
<Text style={{ fontSize: 8 }}>{value}</Text>
</View>
);
const Entry = ({ time, title, a, w, p }) => (
<View>
<View style={{ flexDirection: "row", borderBottomWidth: 1, paddingVertical: 8 }} wrap={false}>
<View style={{ width: 130, marginLeft: 20 }}>
<Text style={{ fontSize: 11, marginBottom: 5 }}>{time}</Text>
<Text style={{ fontSize: 7 }}>PENDING</Text>
</View>
<View style={{ flex: 1, marginRight: 20 }}>
<Text style={{ fontSize: 10 }}>{title}</Text>
<Text style={{ fontSize: 8, marginTop: 5 }}>2 Adults</Text>
<Block label="Address" value={a} />
<Block label="Website" value={w} />
<Block label="Phone" value={p} />
</View>
</View>
</View>
);
const Doc = () => (
<Document>
<Page size="A4" style={{ paddingTop: 40, paddingBottom: 90, paddingHorizontal: 40, flexDirection: "column" }}>
<View fixed>
<Text style={{ fontSize: 22 }}>Daily Itinerary</Text>
</View>
<View style={{ flex: 1 }}>
{/* spacer: pushes the 2nd entry to the bottom of page 1 */}
<View style={{ height: 500 }} />
<View>
<View minPresenceAhead={100}>
<View style={{ marginTop: 10, marginBottom: 8 }}>
<Text style={{ fontSize: 9, fontWeight: 700 }}>TUESDAY, APR 7</Text>
</View>
<Entry time="12:25 AM" title="Juvia"
a="1111 Lincoln Road, Miami Beach, Florida, 33139"
w="http://www.juviamiami.com/" p="+1 305-763-8272" />
</View>
<View>
<Entry time="01:05 AM" title="Miami Beach Golf Club"
a="2301 Alton Road, Miami Beach, Florida, 33140"
w="http://www.miamibeachgolfclub.com/" p="+1 305-532-3350" />
</View>
</View>
</View>
<View style={{ position: "absolute", bottom: 0, left: 40, right: 40, height: 50 }} fixed>
<Text style={{ fontSize: 8 }}>EXCLUSIVE RESORTS</Text>
</View>
</Page>
</Document>
);
ReactPDF.render(<Doc />);The overlap only happens when the entry lands with just barely not enough room, and the exact height that does so depends on font metrics (they differ slightly between the browser REPL and a server render). The verified window is about height: 490–510; if 500 renders cleanly for you, try 490 or 510.
Expected behavior
The entry should move to the next page and render normally — never overlap its own lines / the footer.
Screenshots
Before (published 4.6.1) — page 1: the "01:05 AM / Miami Beach Golf Club" entry is squashed onto the bottom of page 1; Address sits on top of the street, Website on the URL, Phone on the number.
After (fix in #3450) — page 1 / page 2: page 1 ends cleanly after "Juvia"; the golf entry moves to page 2 and renders normally.
Root cause
In resolvePagination's splitNodes, when a node is split and all of its children move to the next page (leaving an empty shell), the parent is kept on the current page whenever currentChildren.length === 0. That check is meant to mean "the page is empty," but currentChildren is the local list for the node's own container — it is 0 simply because the node is the first child of its container, not because the page is empty. So a node that is the sole child of its container but lands on an already-populated page is force-fit into the space left, and Yoga compresses its lines on top of each other.
Environment
- @react-pdf/renderer: ^4.5.1 (uses @react-pdf/layout 4.6.1)
- Node.js server-side rendering (also reproduces in the in-browser REPL)
Proposed fix
#3450 — decide based on whether the page is actually empty above the node (threaded through the split recursion), so a node with content above it moves to the next page; it is only kept (and allowed to overflow) when the page is genuinely empty, which also avoids an infinite page loop. Includes regression tests.
Activity
billinspiratobell commentedon Jul 7, 2026
Investigated this end to end — it reproduces on the latest published version (
@react-pdf/layout4.6.1), there's a clear root cause, and I've opened #3450 with a fix + regression tests.What's happening
A block that lands near the bottom of a page is force-fit into the few points of space left — its lines drawn on top of each other — instead of moving cleanly to the next page. It shows up with two-column rows /
wrap={false}blocks (headers, footers, itinerary-style entries) near a page boundary.Reproduction (react-pdf REPL)
Paste into https://react-pdf.org/repl and look at the bottom of page 1:
Expected (bug): the "01:05 AM / Miami Beach Golf Club" entry at the bottom of page 1 has
Addressdrawn on top of2301 Alton Road…,Websiteon top of the URL, andPhoneon top of the number — every field overlapping the next.With the fix: that entry moves to page 2 intact; page 1 ends cleanly after "Juvia".
Root cause
In
resolvePagination'ssplitNodes, when a node is split and all its children move to the next page (leaving an empty shell), the parent is kept on the current page whenevercurrentChildren.length === 0:currentChildren.length === 0is meant to mean "the page is empty, keep it here to avoid an empty page / infinite loop." ButcurrentChildrenis the local list for the node's own container — it's0just because the node is the first child of its container, which is not the same as the page being empty. So a node that is the sole child of its container but lands on an already-populated page is force-fit into the space left, and Yoga compresses its lines on top of each other.Fix (#3450)
Base the decision on whether the page is actually empty above the node (threaded through the split recursion). Page has content above → move the node to the next page; page genuinely empty and the node can't fit → keep it (allow overflow), which preserves termination and avoids an infinite page loop. Includes regression tests (overlap sweep across page heights + a tall-fixed-header loop guard) and a
REPL.mdwrite-up.docs: add REPL.md with a step-by-step reproduction for diegomura#3449
billnbell3 commentedon Jul 7, 2026
before.pdf
after.pdf
santosch commentedon Aug 5, 2026
Thanks @billinspiratobell for tracking this down and for the fix in #3450 - we hit exactly this in production, and your diagnosis matched what we'd independently traced.
Disclaimer: The following wrapup was written by Claude Code Opus 5, which did most of the investigation and testing automatically, but I (human) have guided, overseen and verified this in person.
@diegomura This is quite the big problem to be honest, would love to get this merged! 🙏
We're hitting this too
Our customer-facing site-survey PDFs came out corrupted after upgrading
@react-pdf/layout3.6.2 → 4.6.1: a block that lands near the bottom of a page gets squeezed into whatever space is left instead of moving to the next page. Worst real case: a photo group rendered at roughly 3% of its intended size — images drawn at6.1 × 8.1ptinstead of188.7 × 251.5pt— with the heading, description and checkbox lines all printed on top of each other.We bisected the exact release that introduced it; details below.
Minimal reproduction
Self-contained, no assets, prints its own verdict:
On
@react-pdf/renderer4.5.1 (@react-pdf/layout4.6.1):With #3450's approach applied:
Note that page count is not a usable signal — the forced placement still emits a nearly empty continuation page, so the document looks plausible until you measure the boxes.
Root cause
Instrumenting
splitNodeson the real failing document:Pagination correctly decides the 567.9pt block must break out of a page with 159.6pt left, and then this overrides that decision:
currentChildren.length === 0is meant to mean "the page is empty", but it is evaluated per parent, not per page — so any sole child looks like an empty page, at any depth. The node is pinned onto a full page, and the relayout insplitPage(which pinspage.style.height) then shrinks it to fit.Regression point:
@react-pdf/layout3.12.0 → 3.13.0We bisected it. Holding
@react-pdf/rendererat 3.4.5 and changing only the layout version:Up to and including 3.12.0, the branch moved the parent to the next page unconditionally:
3.13.0 added the
if (currentChildren.length === 0) { keep it on this page }escape, and that escape is what squashes. The intent looks sound — an un-splittable node that is taller than any page has to be rendered somewhere, otherwise pagination would loop forever. The problem is only thatcurrentChildren.length === 0is a per-container test standing in for a per-page question, so it fires constantly for ordinary sole children on pages that are actually full.That is exactly what #3450 fixes, and it keeps the guard the escape was added for. Worth calling out because a naive "just always break" revert would reintroduce the loop that 3.13.0 was presumably guarding against.
We came from layout 3.6.2 and upgraded to 4.6.1, which crossed that boundary — so for us this arrived as a silent regression in an otherwise routine major upgrade.
Why we'd consider it severe
The trigger cannot be avoided by convention. It is any single-child container, at any depth. "Never render a View with one child" is not a rule a real codebase can hold to — wrappers carry styles, and lists sometimes happen to have exactly one entry. We tried the component-level fix first and rejected it for exactly this reason, and patched the library instead.
The fix works
We applied #3450's approach — threading a real page-empty flag through the split recursion instead of using
currentChildren.length === 0as a proxy — as a localpnpm patchagainst@react-pdf/layout@4.6.1, and verified it against a reproduction built from the real failing document: every image back to its correct188.7 × 251.5pt, no overlapping text, block flows onto the next page.We also checked the opposite case, since that is the risk of any fix here: a non-wrappable block taller than an entire page still renders on a fresh page rather than being moved forever. That is exactly what gating on "everything placed above is
fixed" buys.billnbell3 commentedon Aug 9, 2026
the code is in production for us, and works 100%.
Can we get this merged?