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, canvas, even nested column layouts — 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.

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" }
]
]
}
}