Skip to content

Lay out an HTML table

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.

Terminal window
composer require nextpdf/core:^3

This constraint installs the nextpdf/core package. The example runs on PHP 8.4.

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.

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.

<?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');

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.pdf
  • Percentage widths. Column width percentages 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-collapse default 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.

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 moduleLevelStatusEvidence
CSS Table (css_tables_3)3Verifiedsrc/Html/Table/, tests/Unit/Html/Table/ + TableParser tests + golden PDFs
CSS Flexible Box Layout (css_flexbox_1)1Verifiedsrc/Html/Flex/, tests/Unit/Html/Flex/
CSS Grid Layout (css_grid_1)1Verifiedsrc/Html/Grid/, WPT corpus
CSS Cascading and Inheritance (css_cascade_3)3Verifiedsrc/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.

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().

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.

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.

StatementSpecClausereference_id
Used column widths are determined by the table column-sizing algorithm at the used table width.W3C CSS Table Level 3css_tables_3#x1.x4.x9.x3
In fixed mode, cell content is ignored for column width computation.W3C CSS Table Level 3css_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.

Not applicable.