Skip to content

Paginate a large HTML table across pages

Pass one large <table> to writeHtml(). The engine paginates it across as many PDF pages as the rows need. The <thead> repeats at the top of every page, so each page works as a complete table. This recipe renders a 91-row report that flows across several pages. You do not split the table yourself or compute page breaks.

Terminal window
composer require nextpdf/core

When a table is taller than one page, keep it as a single <table> element. The engine measures each row, fills the usable page height, opens a new page, and continues the same table. It renders the <thead> rows again at the top of each continuation page. Continuation pages keep the document’s top and bottom margins, so the first row on a new page starts below the top margin, not at the page edge.

Place header cells in a <thead> and data rows in a <tbody>. Only the <thead> repeats. Keep each row splittable. A single row taller than the usable page height cannot be paginated and raises UnsplittableContentException.

SymbolLocationRole
Document::writeHtml(string $html): staticNextPDF\Core\Concerns\HasTextOutputRender HTML and paginate tables as needed.
Document::createStandalone(): selfNextPDF\Core\DocumentCreate a standalone document.
Document::addPage(): staticNextPDF\Core\DocumentOpen the first page and set the page size and margins.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$rows = '';
for ($i = 1; $i <= 91; $i++) {
$rows .= "<tr><td>{$i}</td><td>Item {$i}</td><td>In stock</td></tr>";
}
$doc = Document::createStandalone();
$doc->addPage();
$doc->writeHtml(
'<table>'
. '<thead><tr><th>#</th><th>Name</th><th>Status</th></tr></thead>'
. "<tbody>{$rows}</tbody>"
. '</table>'
);
$doc->save(__DIR__ . '/large-table.pdf');

The 91 rows flow across several pages, and the #/Name/Status header repeats on each page.

This self-contained example styles the header, stripes the rows, and writes the PDF to the path the harness supplies.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$rows = '';
for ($i = 1; $i <= 91; $i++) {
$bg = $i % 2 === 0 ? '#F8FAFC' : '#FFFFFF';
$rows .= "<tr style=\"background-color: {$bg};\">"
. "<td style=\"border: 1px solid #CBD5E1; padding: 4px;\">{$i}</td>"
. "<td style=\"border: 1px solid #CBD5E1; padding: 4px;\">Item {$i}</td>"
. "<td style=\"border: 1px solid #CBD5E1; padding: 4px;\">In stock</td>"
. '</tr>';
}
$html = <<<HTML
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: #1E3A8A; color: #FFFFFF;">
<th style="border: 1px solid #1E3A8A; padding: 6px;">#</th>
<th style="border: 1px solid #1E3A8A; padding: 6px;">Name</th>
<th style="border: 1px solid #1E3A8A; padding: 6px;">Status</th>
</tr>
</thead>
<tbody>{$rows}</tbody>
</table>
HTML;
$doc = Document::createStandalone();
$doc->setTitle('Inventory report');
$doc->addPage();
$doc->writeHtml($html);
$out = getenv('NEXTPDF_OUT');
$doc->save($out !== false ? $out : __DIR__ . '/paginate-large-html-tables.pdf');
echo "Wrote the paginated table PDF\n";

Continuation pages use the document’s top and bottom margins. The first row after a page break starts below the top margin, and the last row before a break stops above the bottom margin. This keeps a long table from clipping against the page edge. The same rule applies to a multi-page block and a multi-page table.

  • Pagination warnings are quiet on success. When a table flows cleanly across pages, it emits no warning. The engine raises a TABLE_ROW_OVERFLOW warning only for a degraded layout: when there is no page-break sink, or when a row must be relocated to lead a page. Treat the warning as a signal to simplify the table, not as a failure.
  • Put the header in <thead>. Only <thead> rows repeat. A header row left in <tbody> appears once.
  • Rows must be splittable. A single row taller than the usable page height raises UnsplittableContentException. Break up the content of that row, or shorten it.
  • rowspan degrades across a break. A rowspan cell that crosses a page boundary fragments (ADR-007). For groupings that must remain intact across pagination, use a category-header row instead. See Unsupported CSS features.

Rendering scales linearly with the row count. The engine streams output page by page and never retains a document tree, so memory for a long table does not grow with the page count. The budget for this recipe is wall_ms: 2000, peak_mb: 96.

Validate the row count and cell length of user-supplied data to keep output size bounded. The engine renders text instead of interpreting it, and it runs no script.

StatementSpecClausereference_id
A table header group repeats across the fragmentation containers into which a table is split.W3C CSS Tables 3css_tables_3#x1.x7.x253622ccb1bce2a0cc53bd70919fa4633a9376e2050f63a31a3fde9cb6595ec78

Not applicable.