Skip to main content

Columns

The columns element creates multi-column layouts within a document. Each column accepts any content node — paragraphs, headings, tables, lists, images, SVG, even nested column layouts — and a column item can combine text with a co-located block, following HTML5 flow-content semantics.

Structure​

{
"columns": {
"widths": [1, 1],
"gap": 20,
"rows": [
[
{ "p": "Left column content" }
],
[
{ "p": "Right column content" }
]
]
}
}

Properties​

PropertyTypeRequiredDescription
widthsnumber[]NoPer-column proportional weights (e.g. [3, 1] = 75/25). Picks the proportional grid layout mode.
colNumberintegerNoEqual-split column count. Picks the content-flow layout mode (CSS-multicol semantics — content flows across N columns).
gapnumberNoGap between columns in points (default: 10)
rowsarray[]YesArray of content arrays, one per column (column-major)
stylestringNoNamed style applied to the columns block

Layout modes​

Three branches, each picked by what you wrote:

You wroteLayout modeWhat it's for
widths: [3, 1]Proportional gridSide-by-side panels with explicit ratios. Wins if both widths and colNumber are set.
colNumber: 3Content flowNewspaper-style: a single content stream split across N equal columns.
(neither)Equal gridSide-by-side equal columns; column count = rows.length.

The grid path puts rows[i] in column i. The content-flow path concatenates everything in rows into one stream and lets the renderer break it across colNumber columns.

Column widths​

The widths array defines relative weights. The available page width (minus margins and gaps) is divided according to these ratios.

WidthsLayout
[1, 1]Two equal columns
[2, 1]Two columns, left is twice as wide
[1, 2, 1]Three columns, center is wider
[1, 1, 1]Three equal columns
[3, 1]Two columns, left takes 75%

widths.length drives the column count. If rows has fewer entries than widths.length, the trailing columns render empty. If rows has more, the extras are ignored.

Column gap​

The gap value sets the horizontal spacing between columns, measured in points. If omitted, a default of 10pt is used. In grid mode the gap is split half-and-half between adjacent cells as padding; in content-flow mode the renderer enforces it natively.

Rows​

Each element of rows is an array of content elements rendered in that column — the structure is column-major, so rows[0] is the first column's vertical stack of blocks, rows[1] is the second column's stack, and so on. In the content-flow (colNumber) mode the per-row partitioning is flattened: all content becomes one stream.

The fixture below walks through every routing branch and the common edge cases — default equal grid, proportional widths, float weights, widths mismatched against rows.length, empty cells, widths winning over colNumber, content-flow mode, many narrow columns, and heterogeneous content. Magenta dashed outlines mark each columns block so the boundaries are visible.

Examples​

Two-column layout: sender and recipient​

{
"columns": {
"widths": [1, 1],
"gap": 20,
"rows": [
[
{ "p": "[b]From:[/b]" },
{ "p": "ShipForge Ltd" },
{ "p": "45 Innovation Drive" },
{ "p": "London, UK" }
],
[
{ "p": "[b]To:[/b]" },
{ "p": "Meridian Logistics" },
{ "p": "742 Evergreen Terrace" },
{ "p": "Denver, CO" }
]
]
}
}

Three-column KPI dashboard​

{
"document": {
"styles": {
"kpiLabel": { "fontWeight": "bold", "fontSize": 10, "color": "#333333" },
"kpiValue": { "fontSize": 24, "fontWeight": "bold", "color": "#1A1A2E" },
"kpiNote": { "fontSize": 8, "color": "#999999" }
},
"content": [
{
"columns": {
"widths": [1, 1, 1],
"gap": 15,
"rows": [
[
{ "p": "Revenue", "style": "kpiLabel" },
{ "p": "$1.87M", "style": "kpiValue" },
{ "p": "Target: $1.75M", "style": "kpiNote" }
],
[
{ "p": "Customers", "style": "kpiLabel" },
{ "p": "2,340", "style": "kpiValue" },
{ "p": "Growth: +14.1%", "style": "kpiNote" }
],
[
{ "p": "Satisfaction", "style": "kpiLabel" },
{ "p": "94%", "style": "kpiValue" },
{ "p": "NPS: 72", "style": "kpiNote" }
]
]
}
}
]
}
}

Asymmetric layout: wide content with sidebar​

{
"columns": {
"widths": [3, 1],
"gap": 25,
"rows": [
[
{ "p": "[b]Shipment Details[/b]" },
{ "p": "Your package has been dispatched from the Chicago hub and is currently in transit to the Toronto distribution center. Expected delivery window is April 3-5, 2026." },
{
"table": {
"widths": [1, 2],
"rows": [
[{ "p": "[b]Tracking ID[/b]" }, { "p": "SF-20260330-001" }],
[{ "p": "[b]Weight[/b]" }, { "p": "12.4 kg" }],
[{ "p": "[b]Carrier[/b]" }, { "p": "ShipForge Express" }]
]
}
}
],
[
{ "p": "[b]Quick Links[/b]" },
{
"ul": {
"li": [
{ "p": "Track online" },
{ "p": "Contact support" },
{ "p": "File a claim" }
]
}
},
{ "p": "[barcode, qrcode, SF-20260330-001, 80|80]" }
]
]
}
}

Newspaper-style content flow with colNumber​

When you have a single long content stream that should flow across N equal columns (CSS-multicol semantics), use colNumber instead of widths.

{
"columns": {
"colNumber": 3,
"gap": 14,
"rows": [
[
{ "h3": "Summary" },
{ "p": "The fiscal-year report consolidates revenue, customer growth, and product-line performance across all three operating regions." },
{ "p": "Year-over-year revenue growth reached 18.4%, driven primarily by enterprise renewals and expansion into the Mid-Atlantic corridor." },
{ "p": "Customer retention held at 94.2% — the strongest figure since 2024 — while net-new acquisitions outpaced churn for the eleventh consecutive quarter." }
]
]
}
}

PDF breaks the stream into 3 equal columns with true content reflow. DOCX falls back to a 3-cell equal-width grid since the DOCX format itself has no flow-column primitive at the block level — the result looks identical for content that fits, but won't auto-reflow across columns if the content overflows.

Column-top alignment in content-flow mode​

When a paragraph is mid-flow at a column break, its continuation in the next column rides flush against the column top — paragraph leading isn't reapplied at a continuation. The visible effect: a continuation column's first line can sit a few points higher than columns that start at a paragraph boundary.

This is expected behavior and matches the CSS multicol spec. If you want all column tops to line up exactly:

  • Tip 1 — break only between paragraphs. Author shorter paragraphs sized so the breaker lands at a paragraph boundary every time. The downside: you trade content shape for visual alignment.
  • Tip 2 — equal-height container. Set an explicit height on the columns block; the breaker has more room to balance, reducing mid-paragraph splits.
  • Tip 3 — use proportional grid instead. Switch to widths: [1, 1, 1] (grid mode). Each rows[i] becomes its own column, so every column starts at a paragraph boundary by construction. You lose automatic reflow but gain pixel-aligned tops.

Nesting containers in columns​

A column stacks any content, including a nested table, ul/ol, or another columns layout — and a column item can pair text with a co-located block. This makes each column a full mini-layout, not just a text stack.

{
"columns": {
"widths": [1, 1],
"gap": 16,
"rows": [
[
{ "p": "Balances", "style": "label" },
{
"table": {
"widths": [2, 1],
"rows": [
[{ "td": "Current" }, { "td": "$18,240" }],
[{ "td": "Available" }, { "td": "$1,760" }]
]
}
}
],
[
{ "p": "Action items", "style": "label" },
{ "ol": { "li": [ { "p": "Verify address" }, { "p": "Confirm autopay" } ] } }
]
]
}
}

Data references in columns​

Column content supports $data references just like any other content element.

{
"columns": {
"widths": [1, 1],
"gap": 20,
"rows": [
[
{ "p": "[b]From:[/b] $data.company.name" },
{ "p": "$data.company.address" },
{ "p": "$data.company.city" }
],
[
{ "p": "[b]To:[/b] $data.client.companyName" },
{ "p": "$data.client.address" },
{ "p": "$data.client.city" }
]
]
}
}