Skip to content
getnextpdf.com

stability: Experimental

Retained-mode layout for CSS Grid (grid-template-areas)

Opt-in preview. Retained mode is default-off. The default Streaming mode is byte-identical to a build that never knew this mode existed. Turn it on only for the documents that need a real grid, and validate the result.

By default, the renderer is single-pass and streaming (see ADR-001). A CSS Grid declared with grid-template-areas cannot be placed in one forward pass, so the streaming engine emits an HTML_GRID_REQUIRES_RETAINED warning and falls back to block flow. Retained mode is the opt-in that replaces that fallback with a real layout: Config::withCssLayoutMode(CssLayoutMode::Retained) routes a definite-column grid-template-areas grid through the GridLayoutEngine, which places children into their named cells.

Terminal window
composer require nextpdf/core:^3

The layout mode ships in the core package. The Config::withCssLayoutMode opt-in is @since 6.0.0. The default remains CssLayoutMode::Streaming.

CssLayoutMode is a typed enum on Config. Streaming is the default and the historical behavior; Retained opts a document into the grid engine. Retained mode holds a bounded retained node set (the retainedNodeBudget, default 50,000, clamped to [5,000, 100,000]) so the engine can resolve a grid that streaming cannot — without abandoning the engine’s memory discipline.

When retained mode is on and the engine meets a grid-template-areas grid whose columns are definite, it lays the grid out for real. Definite columns are fixed lengths, percentages, or fr units resolved against the content width. Rows flow automatically. Children are assigned to the cells their area names select.

ADR-001 records the streaming invariant. The 2026-06-28 amendment to ADR-001 adds a retained-opt-in carve-out: the streaming default is untouched and remains the single-pass model; retained mode is an explicitly bounded, opt-in exception for the grid case.

Boundary — what retained mode lays out, and what still falls back

Section titled “Boundary — what retained mode lays out, and what still falls back”

Retained mode handles the definite-column grid-template-areas case and that case only. Everything outside it keeps the HTML_GRID_REQUIRES_RETAINED warning and the block fallback, even with retained mode on:

  • grid-auto-flow: column and grid-auto-flow: dense.
  • subgrid.
  • @container queries.
  • Auto or intrinsic column tracks (auto, min-content, max-content).

These are deferred slices, not silent gaps. A grid that depends on one of them degrades to block flow and tells you so.

Fail-closed boundary. A capture-versus-engine width mismatch — the measured content width disagreeing with the width the grid engine resolves against — fails closed rather than producing a misplaced grid. Retained mode is also incompatible with Safe CSS rendering mode: CssRenderingMode::Safe combined with CssLayoutMode::Retained raises IncompatibleRenderingModeException at config validation. CssLayoutMode::Auto is reserved and raises NotImplementedException.

SymbolLocationRole
Config::withCssLayoutMode(CssLayoutMode $mode): selfsrc/Core/Config.phpOpt a document into Streaming (default) or Retained layout.
Config::withRetainedNodeBudget(int $budget): selfsrc/Core/Config.phpBound the retained node set ([5,000, 100,000], default 50,000).
Config::isRetainedMode(): boolsrc/Core/Config.phpReport whether the document is in retained mode.
CssLayoutModesrc/Core/Streaming, Retained; Auto reserved (NotImplementedException).
GridLayoutEnginesrc/Html/The retained grid placement engine.
IncompatibleRenderingModeExceptionsrc/Exception/Thrown when Safe CSS mode is combined with retained mode.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\CssLayoutMode;
use NextPDF\Core\Document;
$config = (new Config())->withCssLayoutMode(CssLayoutMode::Retained);
$doc = Document::createStandalone($config);
$doc->addPage();
$doc->writeHtml(
'<style>'
. '.dashboard { display: grid; grid-template-columns: 1fr 2fr;'
. ' grid-template-areas: "sidebar main"; }'
. '.sidebar { grid-area: sidebar; } .main { grid-area: main; }'
. '</style>'
. '<div class="dashboard">'
. ' <div class="sidebar">Navigation</div>'
. ' <div class="main">Report content…</div>'
. '</div>',
);
$doc->save(__DIR__ . '/grid.pdf');

Detect the incompatible-mode case at configuration time, and read back the active mode so the path is explicit.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\CssLayoutMode;
use NextPDF\Core\Document;
use NextPDF\Exception\IncompatibleRenderingModeException;
try {
$config = (new Config())
->withCssLayoutMode(CssLayoutMode::Retained)
->withRetainedNodeBudget(75_000);
$config->validate();
} catch (IncompatibleRenderingModeException $e) {
// Safe CSS mode and retained mode cannot combine. Choose one.
throw $e;
}
$doc = Document::createStandalone($config);
assert($config->isRetainedMode());
$doc->addPage();
$doc->writeHtml($gridHtml);
$doc->save($out);
// A grid that needs a deferred feature (column auto-flow, subgrid, @container,
// or intrinsic columns) still emits HTML_GRID_REQUIRES_RETAINED and falls back
// to block flow. Inspect the advisory channel.
  • Streaming stays the default and is byte-identical. Retained mode changes output only for the document you opt in.
  • Only definite-column grid-template-areas. Column auto-flow, dense packing, subgrid, @container, and intrinsic columns keep the HTML_GRID_REQUIRES_RETAINED warning and the block fallback.
  • Safe mode is mutually exclusive. CssRenderingMode::Safe plus CssLayoutMode::Retained throws IncompatibleRenderingModeException.
  • Auto is reserved. CssLayoutMode::Auto raises NotImplementedException; it is not a usable third option yet.
  • Width mismatch fails closed. A capture-versus-engine content-width disagreement is refused, not rendered wrong.

Retained mode holds a bounded node set rather than a full document tree; the retainedNodeBudget (default 50,000) caps it. Grid placement is linear in the node and cell count. The per-page performance_budget (wall_ms: 1500, peak_mb: 64) applies; large grids should keep the budget in mind when raising the node budget toward its 100,000 ceiling.

Retained mode does not widen the input surface. The HTML security policy, CSS property allowlist, and parser caps apply unchanged. The retained node budget is itself a resource-exhaustion bound: it caps how much structure the engine will hold for a single document.

StatementSpecClause
grid-template-areas names grid cells; named areas place items.W3C CSS Grid Layout Module Level 1§7.3
Explicit fixed, percentage, and fr tracks size against the content width.W3C CSS Grid Layout Module Level 1§7.2

This is a preview implementation of a definite-column grid-template-areas subset. Per-property verified status is tracked in the CSS support matrix; no end-to-end conformance is claimed here. No standards text is reproduced.