Skip to main content
Forme’s layout engine is page-native. Every layout decision, from flex calculations to text wrapping, is made with the page boundary as a hard constraint. This page explains how content flows across pages and how to control that behavior.

Why CSS page breaks are unreliable

If you have tried generating PDFs from HTML, you have probably run into this: you add page-break-inside: avoid or break-before: always to your CSS, and the browser ignores it. This is not a bug in your code. CSS page break properties (page-break-inside, break-before, break-after) are hints to the rendering engine, not guarantees. Chrome and other browsers frequently ignore them, especially with complex layouts, flexbox containers, or tables. This is a fundamental limitation of the HTML-to-PDF approach. The browser lays out content for a scrollable viewport, then tries to slice it into pages after the fact. Page breaks are an afterthought, not a first-class concern. Forme does not use CSS for layout. Every element is measured and positioned in a page-aware coordinate system. When content exceeds the page boundary, the engine splits it deterministically. There is no guessing, no hoping, and no browser-specific behavior to work around.

Automatic page breaks

When content exceeds the available space on a page, Forme automatically moves it to a new page. This happens at natural boundaries:
  • Between children of a View container
  • Between rows of a Table
  • Between lines of a Text block
<Page size="Letter" margin={54}>
  {/* If these items don't all fit on one page, they flow to the next */}
  <View>
    <Text>Item 1</Text>
    <Text>Item 2</Text>
    {/* ...more items... */}
    <Text>Item 50</Text>
  </View>
</Page>
You do not need to calculate page heights or manually manage pagination. The engine handles it.

Manual page breaks

Use <PageBreak /> to force content onto a new page:
<Text style={{ fontSize: 24, fontWeight: 700 }}>Chapter 1</Text>
<Text>Chapter content...</Text>

<PageBreak />

<Text style={{ fontSize: 24, fontWeight: 700 }}>Chapter 2</Text>
<Text>Next chapter starts on a fresh page.</Text>
You can also use the breakBefore style property:
<View style={{ breakBefore: true }}>
  <Text style={{ fontSize: 24, fontWeight: 700 }}>Chapter 2</Text>
</View>

Non-breakable elements

Set wrap={false} on a View to prevent it from splitting across pages. If the element doesn’t fit on the current page, it moves entirely to the next page.
<View wrap={false} style={{ padding: 16, backgroundColor: '#f8fafc', borderRadius: 8 }}>
  <Text style={{ fontWeight: 700 }}>Key Finding</Text>
  <Text>This card and all its content will always appear on the same page.</Text>
  <Text>If it doesn't fit in the remaining space, the whole card moves to the next page.</Text>
</View>
This is useful for cards, summary blocks, and other elements where splitting would look wrong.

Widow and orphan control

When a text paragraph splits across pages, Forme ensures a minimum number of lines appear on each side of the break. This prevents a single isolated line at the bottom of a page (orphan) or at the top of the next page (widow). By default, at least 2 lines are kept on each side. You can adjust this per element:
{/* Default: at least 2 lines on each side of a break */}
<Text>Long paragraph that might split across pages...</Text>

{/* Stricter: at least 3 lines on each side */}
<Text style={{ minWidowLines: 3, minOrphanLines: 3 }}>
  Important text where more context is needed on each page...
</Text>
If only 1 line would fit at the bottom of a page (and minOrphanLines is 2), the entire paragraph moves to the next page. If a split would leave only 1 line at the top of the next page (and minWidowLines is 2), the split point moves up to keep at least 2 lines on the next page.

Fixed headers and footers

Use <Fixed> to repeat content on every page. Fixed elements reduce the available content area.
<Page size="Letter" margin={54}>
  <Fixed position="header">
    <View style={{ flexDirection: 'row', justifyContent: 'space-between', paddingBottom: 8, borderWidth: { top: 0, right: 0, bottom: 1, left: 0 }, borderColor: '#e2e8f0' }}>
      <Text style={{ fontSize: 10, fontWeight: 700 }}>Acme Corp</Text>
      <Text style={{ fontSize: 10, color: '#94a3b8' }}>Annual Report 2025</Text>
    </View>
  </Fixed>

  <Fixed position="footer">
    <View style={{ paddingTop: 8, borderWidth: { top: 1, right: 0, bottom: 0, left: 0 }, borderColor: '#e2e8f0' }}>
      <Text style={{ fontSize: 9, textAlign: 'center', color: '#94a3b8' }}>
        Page {'{{pageNumber}}'} of {'{{totalPages}}'}
      </Text>
    </View>
  </Fixed>

  {/* Content area is reduced by header and footer height */}
  <Text>Document content here...</Text>
</Page>
Fixed elements appear in the page margin area (between the page edge and the content boundary). They are drawn on every page that the parent <Page> produces.
import { Document, Page, View, Text, Fixed, Table, Row, Cell } from '@formepdf/react';

export default function Report(data) {
  return (
    <Document>
      <Page size="A4" margin={54}>
        {/* Repeating header */}
        <Fixed position="header">
          <View style={{ flexDirection: 'row', justifyContent: 'space-between', paddingBottom: 12, borderWidth: { top: 0, right: 0, bottom: 1, left: 0 }, borderColor: '#e2e8f0' }}>
            <Text style={{ fontSize: 10, fontWeight: 700, color: '#1e293b' }}>My Company</Text>
            <Text style={{ fontSize: 9, color: '#94a3b8' }}>Confidential</Text>
          </View>
        </Fixed>

        {/* Repeating footer with page numbers */}
        <Fixed position="footer">
          <View style={{ flexDirection: 'row', justifyContent: 'space-between', paddingTop: 12, borderWidth: { top: 1, right: 0, bottom: 0, left: 0 }, borderColor: '#e2e8f0' }}>
            <Text style={{ fontSize: 9, color: '#94a3b8' }}>formepdf.com</Text>
            <Text style={{ fontSize: 9, color: '#94a3b8' }}>
              Page {'{{pageNumber}}'} of {'{{totalPages}}'}
            </Text>
          </View>
        </Fixed>

        {/* Page content — flows across as many pages as needed */}
        <Text style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>Quarterly Report</Text>
        <Table columns={[{ width: { fraction: 0.5 } }, { width: { fraction: 0.5 } }]}>
          <Row header style={{ backgroundColor: '#1e293b' }}>
            <Cell style={{ padding: 8 }}><Text style={{ color: '#fff', fontWeight: 700 }}>Item</Text></Cell>
            <Cell style={{ padding: 8 }}><Text style={{ color: '#fff', fontWeight: 700 }}>Amount</Text></Cell>
          </Row>
          {data.items.map((item, i) => (
            <Row key={i}>
              <Cell style={{ padding: 8 }}><Text>{item.name}</Text></Cell>
              <Cell style={{ padding: 8 }}><Text>{item.amount}</Text></Cell>
            </Row>
          ))}
        </Table>
      </Page>
    </Document>
  );
}
The header and footer repeat on every page automatically. The content area between them is reduced accordingly, so you don’t need to calculate available space.

Watermarks

For text that appears behind content on every page, use the <Watermark> component instead of <Fixed>:
<Page>
  <Watermark text="DRAFT" fontSize={60} color="rgba(0,0,0,0.08)" angle={-45} />
  <Text>Content renders on top of the watermark.</Text>
</Page>

Dynamic page numbers

Use these placeholders in any <Text> element:
PlaceholderDescription
{{pageNumber}}Current page number (1-based)
{{totalPages}}Total number of pages in the document
<Text>Page {'{{pageNumber}}'} of {'{{totalPages}}'}</Text>
Page numbers are resolved after the full layout pass, so {{totalPages}} is always accurate. These are commonly used inside <Fixed> elements but work anywhere.

Table header repetition

When a table spans multiple pages, header rows (marked with header) are automatically repeated at the top of each continuation page.
<Table columns={[{ width: { fraction: 0.6 } }, { width: { fraction: 0.4 } }]}>
  <Row header style={{ backgroundColor: '#1e293b' }}>
    <Cell style={{ padding: 8 }}><Text style={{ color: '#fff', fontWeight: 700 }}>Product</Text></Cell>
    <Cell style={{ padding: 8 }}><Text style={{ color: '#fff', fontWeight: 700 }}>Revenue</Text></Cell>
  </Row>
  {/* If these 100 rows span 3 pages, the header appears on all 3 */}
  {data.map((row, i) => (
    <Row key={i}>
      <Cell style={{ padding: 8 }}><Text>{row.product}</Text></Cell>
      <Cell style={{ padding: 8 }}><Text>{row.revenue}</Text></Cell>
    </Row>
  ))}
</Table>
No configuration needed. Mark a row as header and it repeats automatically.

Flex layout across page breaks

This is the core differentiator from other PDF tools. When a flex container splits across pages, Forme runs independent flex calculations for each page fragment. Consider a row layout with three items where the container splits after the second item:
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
  <View style={{ flexGrow: 1 }}><Text>Item 1</Text></View>
  <View style={{ flexGrow: 1 }}><Text>Item 2</Text></View>
  <View style={{ flexGrow: 1 }}><Text>Item 3</Text></View>
</View>
In Forme, each page fragment gets its own flex pass. Items on page 1 fill that page’s width correctly, and items on page 2 fill that page’s width correctly. In tools that use the infinite-canvas-then-slice approach, flex runs once on the full container, then the result is sliced. This produces incorrect widths on both pages because the flex math assumed all items were on one line.

How it differs from react-pdf

react-pdf lays out content on an infinite vertical canvas and then slices it into pages. This causes several problems:
  1. Flex breaks on page boundaries. A flex row that gets sliced has its distribution calculated for the full container, then cut in half. Both halves have wrong proportions.
  2. Tables break mid-row. Without page-aware row placement, a table row can be sliced between its top and bottom border.
  3. No header repetition. Since the table is just a set of rectangles on an infinite canvas, there is no concept of “repeat this row at the top of each page.”
  4. No post-split adjustment. After slicing, there is no second layout pass to fix the fragments. What you see is the result of a single layout pass on the wrong dimensions.
Forme avoids all of these problems because the page is the fundamental unit of layout. Every decision is made with the page boundary in mind from the start.