renderDocumentWithLayout() returns two things: the rendered PDF bytes, and a LayoutInfo object describing every laid-out node with its position, size, style, and children. This page documents the shape of that layout tree and the helpers for querying it.
Two APIs — pick the right one
Prefer the helpers (@formepdf/core/layout) for common queries. They encapsulate the layout-time transforms that would otherwise be invariants you have to remember — “text is on TextLine children, not the parent Text block”, “there is no Table wrapper node”, and so on. The helpers make those invariants a maintained API surface instead of a documented convention.
Fall back to raw ElementInfo tree access when you need it. For custom snapshot comparison, structural analysis the helpers don’t cover, or writing your own traversal. The tree is always there; helpers are additive.
The layout-time transforms are documented on the ElementInfo JSDoc and enforced by a runtime-conformance test — if any of them change, the test in @formepdf/core fails immediately.
Helpers
Text access
The load-bearing case. Text content lives onTextLine leaf nodes — not on the parent Text block — because the layout engine splits blocks into wrapped lines during rendering. Consumers that read textBlock.textContent get null and are confused.
getNodeText(node) — read the text of a subtree
TextLine descendant’s text, joined with "\n". If the layout wrapped a source string across multiple lines, they come back separated by newlines. Strip them if you don’t want that: .replace(/\n/g, ' ').
If node is itself a TextLine, returns its own text.
getTextLines(node) — get the line-by-line array
Structural queries
Each of these encapsulates one of the documented layout-time transforms.getHeadingLevel(node) — 1–6 or null
H1–H6), not a generic Heading node with a level field.
getTableRows(parent) — direct TableRow children
<Table> unwraps at layout time — its <Row> children become sibling TableRow nodes on the containing page/View, and there is no Table wrapper node.
Accepts a PageInfo or ElementInfo (e.g. a View that contained the <Table> in JSX).
getFixedRegions(page) — { header, footer } arrays
<Fixed position="header"> produces FixedHeader nodes and <Fixed position="footer"> produces FixedFooter — no single Fixed nodeType.
getListItems(list) + getListItemMarker(item)
Lbl children of each ListItem rather than a field on ListItem.
Traversal
walkElements(root, cb) — depth-first walk
LayoutInfo, PageInfo, ElementInfo, or an array of any of those. The callback receives the node and a human-readable path string (e.g. "[0].children[3].children[1]"). Return false to skip descent into that node’s children.
findElements(root, predicate) — filter
findFirstElement(root, predicate) — one match
null if no match. Stops descent as soon as a match is found (does not recurse into the match itself).
isNodeType(nodeType) — type-guard for filter chains
Raw ElementInfo tree
If you need to walk the tree yourself — e.g. for custom snapshot comparison, structural analysis the helpers don’t cover, or shipping the layout data to another process — the raw tree is always available. Every claim below is enforced by a runtime-conformance test in @formepdf/core (tests/layout-shape.test.ts); if it drifts, the test fails before it ships.
Top-level shape
ElementInfo
Layout-time transforms (the invariants)
<Table>is unwrapped. Its<Row>children appear as siblingTableRownodes on the containing page/View. There is noTablewrapper node.<OrderedList>and<UnorderedList>both produceListnodes containingListItemchildren. EachListItemhas aLblchild (the marker:"1."/"•") followed by the item’s content.<Fixed position="header">producesFixedHeaderand<Fixed position="footer">producesFixedFooter. There is no singleFixednodeType.- Headings emit six discrete
H1–H6nodeTypes. There is no genericHeadingwith alevelfield. <Text>block content is split intoTextLineleaves. The actual text lives onTextLine.textContent; non-TextLinenodes (including the parentTextblock) emitnullfortextContent.- Inline elements (
<Strong>,<Em>,<Code>,<Link>) do not appear as their own nodes. They contribute style runs withinTextLine. <PageBreak>produces no node. It triggers a page break during layout and is otherwise invisible.
Enum values
The layout engine serializes style enums as PascalCase strings (Rust convention). This is not the same as the CSS-style camelCase values you author with instyle props:
if (style.flexDirection === 'row') fails to typecheck because 'row' isn’t a member of ElementFlexDirection.
Every enum union is exported for narrowing:
Drift protection
If any of the layout-time transforms change in a future release, the runtime-conformance test in@formepdf/core fails first — before the release ships. That test explicitly asserts each transform on a rich fixture and reports the specific transform and node that broke. When it fires, this docs page and the JSDoc on ElementInfo get updated in the same commit.
Consumers depending on the helpers ride through most of these changes transparently — that’s the whole reason they exist. Consumers depending on the raw tree get the update in the exported types and a note in the CHANGELOG.