Lay out an HTML table
At a glance
Section titled “At a glance”Use this recipe to render a Hypertext Markup Language (HTML) table with a header, aligned cells, borders, and a footer row. Tables are a Verified module in the Cascading Style Sheets (CSS) support matrix. The recipe follows examples/09-html-table.php.
Install
Section titled “Install”composer require nextpdf/core:^3This constraint installs the nextpdf/core package. The example runs on PHP 8.4.
Conceptual overview
Section titled “Conceptual overview”The HTML engine uses a dedicated table pipeline (src/Html/Table/). TableParser collects rows and cells in a short-lived buffer, then NextPDF lays out and paints the table. This is the single acknowledged deviation from the no-retained Document Object Model (DOM) model in ADR-001. The buffer is short-lived and scoped to its container. It is not a persistent DOM.
Column widths follow the CSS Table model. NextPDF lays out the table at a given used width, then the column-sizing algorithm sets the used width of each column (W3C CSS Table Level 3). With table-layout: fixed, cell content is ignored for width computation. Instead, the first row and the explicit column widths drive the layout (W3C CSS Table Level 3).
CSS Table Level 3 is graded Verified in the matrix. Four sources support this grade: src/Html/Table/, the Table unit suite, the TableParser tests, and synthetic golden PDFs.
API surface
Section titled “API surface”Render tables with writeHtml(string $html): static (NextPDF\Core\Concerns\HasTextOutput). The engine supports standard table markup. Supported tags are table, thead, tbody, tfoot, tr, th, and td. Supported attributes are border, cellpadding, cellspacing, colspan, and a per-cell style. The full PHPDoc table is generated from the source.
Code sample — Quick start
Section titled “Code sample — Quick start”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->addPage();
$doc->writeHtml( '<table border="1" cellpadding="5"><tr><th>SKU</th><th>Qty</th></tr>' . '<tr><td>NPD-CORE</td><td style="text-align: right;">1</td></tr></table>');
$doc->save(__DIR__ . '/out.pdf');Code sample — Production
Section titled “Code sample — Production”This self-contained example can run in the harness. It mirrors examples/09-html-table.php and shows a styled header, alternating row backgrounds, aligned numeric columns, and a footer total.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();$doc->setTitle('HTML Table');$doc->addPage();
$html = <<<'HTML'<h1 style="color: #1E3A8A;">Product Inventory Report</h1>
<table border="1" cellpadding="5" cellspacing="0" style="width: 100%;"> <thead> <tr style="background-color: #1E3A8A; color: #FFFFFF;"> <th style="width: 10%;">#</th> <th style="width: 45%;">Product</th> <th style="width: 20%; text-align: center;">SKU</th> <th style="width: 25%; text-align: right;">Price</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>NextPDF Core License</td> <td style="text-align: center;">NPD-CORE</td> <td style="text-align: right;">$0.00</td> </tr> <tr style="background-color: #F8FAFC;"> <td>2</td> <td>NextPDF Pro License</td> <td style="text-align: center;">NPD-PRO</td> <td style="text-align: right;">$299.00</td> </tr> </tbody> <tfoot> <tr style="background-color: #1E293B; color: #FFFFFF; font-weight: bold;"> <td colspan="3" style="text-align: right;">Grand total:</td> <td style="text-align: right;">$299.00</td> </tr> </tfoot></table>HTML;
$doc->writeHtml($html);
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$doc->save($out !== false ? $out : __DIR__ . '/html-table-layout.pdf');
echo "Wrote html-table-layout.pdf\n";Expected standard output (STDOUT):
Wrote html-table-layout.pdfEdge cases & gotchas
Section titled “Edge cases & gotchas”- Percentage widths. Column
widthpercentages resolve against the used table width. The column-sizing algorithm normalizes percentages that do not add up to 100. Do not assume exact pixel widths. colspan. A spanning cell participates in width distribution across the columns it covers. A footer total that spans most columns is a common, supported pattern.- Border model. In some table fixtures, the
border-collapsedefault differs from the CSS initial value. Set it directly when border rendering matters. - Rowspan across page breaks. A rowspanning cell can cross a page boundary, and the rowspan paginator then fragments that cell (ADR-007). A very tall cell that cannot be split can raise
UnsplittableContentException. - Empty cells. An empty
<td>still occupies its grid slot. It is not collapsed away.
Performance
Section titled “Performance”The table buffer is short-lived and scoped to its container. Parsing cost is linear in the cell count, plus the column-sizing pass at O(rows x cols). The budget is wall_ms: 1500, peak_mb: 96. Keep a single table within the ADR-020 node bound of 5,000 nodes per context.
CSS support matrix excerpt (Verified-only rows)
Section titled “CSS support matrix excerpt (Verified-only rows)”This excerpt includes only the Verified rows from the truth-audited CSS support matrix.
| W3C module | Level | Status | Evidence |
|---|---|---|---|
CSS Table (css_tables_3) | 3 | Verified | src/Html/Table/, tests/Unit/Html/Table/ + TableParser tests + golden PDFs |
CSS Flexible Box Layout (css_flexbox_1) | 1 | Verified | src/Html/Flex/, tests/Unit/Html/Flex/ |
CSS Grid Layout (css_grid_1) | 1 | Verified | src/Html/Grid/, WPT corpus |
CSS Cascading and Inheritance (css_cascade_3) | 3 | Verified | src/Html/Cascade/ |
background-color, used here for row striping, is graded “Claimed” in the matrix. Under the Phase 0 audit, its scope is the table cell. It renders for table rows, but it is not listed as Verified.
Single-pass streaming constraints (ADR-001)
Section titled “Single-pass streaming constraints (ADR-001)”The table buffer is the documented exception to the streaming model. It is short-lived and bounded, not a general DOM. Do not rely on a table to provide tree context for content outside the table.
Layer contracts (ADR-010)
Section titled “Layer contracts (ADR-010)”FormattingContextFactory::startTable() reads CSS through the layout layer’s contract. It does not parse raw $css[...] in the dispatch path. The public surface remains writeHtml().
Memory budget for large documents
Section titled “Memory budget for large documents”A table formatting context is bounded to 5,000 nodes and 20 levels deep, within the 50 MB active-memory ceiling (ADR-020). Split or paginate a very large table. Do not render it as one oversized context.
Security notes
Section titled “Security notes”Table markup from untrusted input is bounded by the same element and nesting caps as other HTML. Validate table data that a user assembles. The renderer does not execute content, and under the default policy, it does not fetch arbitrary remote resources.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
| Used column widths are determined by the table column-sizing algorithm at the used table width. | W3C CSS Table Level 3 | css_tables_3#x1.x4.x9.x3 | |
| In fixed mode, cell content is ignored for column width computation. | W3C CSS Table Level 3 | css_tables_3#x1.x4.x5.x1.p6 |
This recipe shows how NextPDF renders supported HTML tables. CSS Table Level 3 is Verified in the support matrix, and the other CSS modules used here are graded by that matrix.
Commercial context
Section titled “Commercial context”Not applicable.