If Flexbox solves one-dimensional arrangement, Grid solves two: card walls, report matrices, price tables, and schedules all lay out in a few lines, with column widths dividing themselves, and spanning and placement under direct control. This is a level of productivity that table layouts and absolute positioning cannot approach. dompdf.js supports the Grid layout model, rendering grid-template-columns, grid-template-rows, gap, grid-column, and grid-row according to standard CSS, which makes complex layouts in frontend PDF development dramatically simpler and lets you reuse grid designs already built for the web. This article starts with the core concepts of Grid, grid lines and tracks, then covers the container properties, placement and spanning syntax, with runnable code examples for report grids and card walls. Dedicated sections explain how Grid cooperates with pagination, how to mix Grid and Flexbox in one template, and a troubleshooting plus fallback guide for environments with partial Grid support. By the end, complex PDF layouts stop being a grind of manual coordinates and become a matter of configuration, freeing your time for the content that actually matters.Beyond the syntax, you will learn how to think about track sizing with the fr unit, how minmax() keeps content readable, and how to keep grids stable when they cross page boundaries. The pagination section is especially important for reports, because a grid that renders beautifully on one page can surprise you when it spans several. Every pattern here is paired with a runnable example, and the troubleshooting guide is organized by symptom so you can jump straight to the fix you need.
Grid layout works by establishing grid lines inside a container; those lines delimit tracks, rows and columns, and items are placed into the cells where tracks intersect. dompdf.js computes track sizes with the standard box model, so a container with display: grid enters grid layout mode and its direct children become grid items.
Line numbering is the key to placement: lines are numbered from 1 at the container's start edge, so grid-column: 1 / 3 makes an item span from line 1 to line 3, occupying two columns. Negative numbers count from the end, and grid-column: 1 / -1 spans the full width, the canonical trick for full-row elements inside a grid.
Unlike the one-dimensional thinking of Flexbox, Grid considers both dimensions at once: row tracks come from grid-template-rows and column tracks from grid-template-columns, each independently accepting fixed lengths, percentages, and the fr elastic unit, and the combinations cover an enormous range of structures.
The mental shift from tables to Grid is worth making explicit: tables impose a rigid matrix where every row shares the same column structure, while Grid lets each item occupy an arbitrary rectangle. That freedom is what enables merged report cells, feature cards, and schedules without the hacks that table layouts require. Once you start designing with rectangles instead of rows, the layout vocabulary expands immediately, and templates become shorter and clearer.
The fr unit deserves special respect because it changes how tracks behave under content pressure: unlike percentages, fr tracks share only the space left after fixed and auto tracks are resolved, and they can shrink below their content unless protected. When a grid column looks too narrow, the fix is usually minmax(120px, 1fr) rather than a larger percentage, which both guarantees a floor and keeps the elastic behavior.
grid-template-columns defines the column tracks: repeat(3, 1fr) yields three equal columns, where 1fr is the elastic unit that distributes remaining space proportionally; mixing fixed and elastic values, such as 200px 1fr 2fr, creates classic sidebar-plus-main layouts that behave identically to the browser.
gap sets both row and column spacing in one declaration and is the standard spacing property for grids, replacing the legacy grid-gap name. grid-auto-rows sizes implicit rows, and combined with minmax(80px, auto) it bounds the height range of automatically generated rows, which suits lists whose item heights vary with content.
grid-auto-flow controls the automatic placement direction: row fills row by row by default, column fills column by column, and the dense value back-fills holes, useful when items have unequal sizes. Understanding auto-placement saves you from confusing item positions and should be the first thing you check when items land somewhere unexpected.
repeat() has a few forms worth knowing beyond the basic count: repeat(auto-fill, minmax(160px, 1fr)) creates as many columns as fit, which is the canonical responsive card wall, and repeat(2, 1fr 2fr) repeats the pattern, producing alternating column widths. These forms keep templates compact and self-adjusting, and they are the reason Grid templates stay short even for complex layouts. When the page width is fixed, auto-fill simply settles on the number of columns that fit, which is exactly what you want in a PDF.
Track sizing also needs a decision about rows: grid-auto-rows with a fixed value keeps implicit rows uniform, while minmax lets them breathe with content. For report grids, uniform rows look cleaner and make pagination predictable, so prefer a fixed or tightly bounded auto-rows value. For card walls, auto is fine, because each card defines its own height and the grid just aligns them.
Items can be placed explicitly: grid-column: 1 / 3 spans two columns, grid-row: 1 / 3 spans two rows, or you can use the span keyword, grid-column: span 2, which avoids counting line numbers entirely and makes template adjustments smaller and more readable, so it is the recommended form for daily work.
grid-area combines all four placement values: grid-area: 1 / 1 / 3 / 3 sets row start, column start, row end, and column end in one declaration. Paired with grid-template-areas, which names regions with an ASCII-art style map such as header, sidebar, main, and footer, you get a highly readable description of the layout, ideal for templates with fixed structure.
Spanning is Grid's decisive advantage over tables: a table cannot let a cell span rows and columns without breaking its structure, while Grid composes freely. Merged report cells, feature cards in a wall, and cross-time-slot events on a schedule are all a few declarations away, with far lower maintenance cost than the traditional approaches.
Named areas are the most readable form of grid placement, but they come with a rule: every cell of the map must be filled with either a name or a dot, and the same name may appear in multiple cells to span them. The payoff is a template whose structure is visible at a glance, and changing the layout means editing the ASCII map rather than hunting through placement declarations. For documents with a stable skeleton, such as reports with a header, sidebar, and main area, named areas are the clearest choice.
Explicit placement with numbers and spans remains the right tool for data-driven grids, where rows are generated in a loop and each item knows its own span. The pattern is simple: give each generated item its grid-column and grid-row values from the data, and the grid assembles itself. Keep placement logic in one place, ideally a small helper, so the template stays readable as the number of generated items grows.
The example demonstrates the two most common grid patterns: a report matrix with 2fr 1fr 1fr columns, a one-pixel gap that doubles as grid lines, and a full-width totals row via grid-column: 1 / -1, plus a three-column card wall with a feature card spanning two columns. Notice how the gap technique turns the container background into the grid lines, which paginates more cleanly than per-cell borders. Run it as-is, then change the track definitions to see the layout reflow.
The feature card's span 2 demonstrates why spanning beats tables: the wall stays a clean three-column grid while one cell claims more width, and nothing else shifts. In a table, the same effect would require colspan gymnastics and careful row alignment. This asymmetry is Grid's core advantage, and it shows up in every report that needs a highlighted row or a merged summary cell.
import { DomPDF } from 'dompdf.js';
const html = `
<style>
.report {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 1px;
background-color: #b8c2cc; /* gaps act as grid lines */
border: 1px solid #b8c2cc;
}
.report > div {
background-color: #fff;
padding: 10px;
}
.report .head {
background-color: #2c3e50;
color: #fff;
font-weight: bold;
}
.report .total {
grid-column: 1 / -1; /* span the full row */
text-align: right;
font-weight: bold;
}
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 20px;
}
.cards .feature {
grid-column: span 2; /* feature card spans two columns */
background-color: #eaf2fb;
}
</style>
<div class='report'>
<div class='head'>Item</div><div class='head'>Qty</div><div class='head'>Amount</div>
<div>Mechanical keyboard</div><div>1</div><div>$ 399</div>
<div>Monitor</div><div>2</div><div>$ 3,998</div>
<div class='total'>Total $ 4,397</div>
</div>
<div class='cards'>
<div class='feature'>Featured product (spans 2)</div>
<div>Accessories</div>
<div>Service & warranty</div>
</div>`;
const pdf = new DomPDF({ format: 'A4', margin: '20mm' });
pdf.addPage(html, { format: 'A4' });
pdf.save('grid-demo.pdf');
Grid for the skeleton and Flexbox for the details is the golden combination of modern layout: Grid builds the row-and-column frame of a report while flex aligns icons and text inside each cell. This layering works just as well in PDF templates, keeps the structure clear and maintainable, and is the recommended architecture for complex layouts.
When a grid spans pages, dompdf.js finds break points along row tracks, so grids with stable row heights paginate cleanly. Give repeating rows a stable height, either a fixed value or a minmax lower bound, so row heights do not fluctuate with content and the number of rows per page stays predictable.
Spanning items can get clipped at page boundaries, so keep multi-row merged cells away from page edges and toward the middle of the data area. For long reports, the most controllable approach is splitting the data source by page and building one grid per page rather than relying on automatic splitting, which gives you exact control over what appears where.
Grids paginate by row tracks, so the row height strategy determines the output quality: fixed or tightly bounded rows produce even page fills, while wildly varying rows create ragged page ends. For multi-page reports, compute rows per page from the content area height and the row height, and either paginate the data source or accept the engine's natural breaks. Consistency of row height is the single highest-leverage factor in clean grid pagination.
Spanning items near page boundaries are the main risk: a row that spans two columns and crosses a page break can render awkwardly or clip. Keep spanning elements away from expected break lines, or restructure so spans stay within a single page's worth of rows. When a report mixes spanning and pagination heavily, consider building one grid per page from pre-sliced data, which trades a little code for complete control over every page.
Q: Grid column widths do not match expectations? A: Check whether fixed values and percentages mix with fr units; fr distributes only the remaining space after fixed tracks are resolved. If content overflows, set min-width: 0 on the grid container or adjust the track definitions to restore the expected distribution.
Q: Items land in the wrong cells? A: Check grid-auto-flow and the implicit tracks: when items exceed the explicitly defined rows and columns, implicit tracks are generated and sized by grid-auto-rows or grid-auto-columns. Explicit placement removes the ambiguity entirely.
Q: Content gets clipped when the grid paginates? A: Give repeating units a stable row height and check whether spanning items cross a page boundary. Keep high-risk content in the middle of the data area or split the data source per page, and pagination becomes predictable.
Fallback: for environments with incomplete Grid support, table layout reproduces equal-width grids and flex with percentages reproduces elastic grids. The fallback templates are longer but functionally equivalent, so reserve them for compatibility cases and standardize on Grid for new templates.
The most common grid bug is the overflow puzzle: content wider than its track, often a long word or an unbroken string, pushes the track wider than intended and breaks the column balance. The fix is min-width: 0 on the grid items or minmax() floors on the tracks, which let content wrap instead of inflating the track. Apply the same defensive sizing you use in flex, and the columns stay honest.
When a grid renders differently in the PDF than in the browser, check three suspects in order: track definitions using legacy syntax such as grid-gap, auto-placement assumptions that change with content length, and fonts whose metrics differ and shift the wrap points. Isolate the grid in a minimal snippet, render it in both environments, and compare the computed track widths, which separates engine behavior from template mistakes in minutes.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。