Skip to main content
A table is a grid of columns with an optional header row and a body that repeats one row per element in an array from your data. Repeating table rows is the only loop in the template language — there are no other loops or conditionals.
{
  "type": "table",
  "columns": [
    { "kind": "relative", "weight": 5 },
    { "kind": "relative", "weight": 1 },
    { "kind": "relative", "weight": 2 }
  ],
  "header": {
    "row": [
      { "child": { "type": "text", "value": "Item", "style": { "semiBold": true } }, "border": { "bottom": 1, "color": "grey.darken1" }, "padding": 4 },
      { "child": { "type": "text", "value": "Qty", "style": { "semiBold": true }, "align": "right" }, "border": { "bottom": 1, "color": "grey.darken1" }, "padding": 4 },
      { "child": { "type": "text", "value": "Price", "style": { "semiBold": true }, "align": "right" }, "border": { "bottom": 1, "color": "grey.darken1" }, "padding": 4 }
    ]
  },
  "body": {
    "forEach": "receipt.items",
    "row": [
      { "child": { "type": "text", "value": "{{item.name}}" }, "padding": 2 },
      { "child": { "type": "text", "value": "{{item.qty}}", "align": "right" }, "padding": 2 },
      { "child": { "type": "text", "value": "{{item.price}}", "align": "right" }, "padding": 2 }
    ]
  }
}
FieldTypeRequiredNotes
columnsColumn[]yesAt least one. Defines the grid’s width structure.
header{ row: Cell[] }noA header row drawn once at the top (and repeated atop each page for a multi-page table).
body{ forEach, row }yesThe repeating part — see below.

Columns

Columns are sized exactly like row cellsrelative by weight, or constant by width:
"columns": [
  { "kind": "relative", "weight": 5 },
  { "kind": "constant", "width": 80 }
]
The number of columns sets the grid; every header and body row must supply exactly that many cells.

Repeating rows with forEach

The body’s forEach is a dotted path into data that must resolve to an array. Tipar renders the row template once per element, binding the current element to item:
"body": {
  "forEach": "invoice.lines",
  "row": [
    { "child": { "type": "text", "value": "{{item.description}}" } },
    { "child": { "type": "text", "value": "{{item.amount}}", "align": "right" } }
  ]
}
Given this data, the body renders three rows:
{
  "invoice": {
    "lines": [
      { "description": "Pro plan", "amount": "€49.00" },
      { "description": "Extra seats", "amount": "€30.00" },
      { "description": "SMS credits", "amount": "€8.00" }
    ]
  }
}
item is only defined inside a table body. Referencing {{item.x}} anywhere else, or pointing forEach at something that isn’t an array, fails with 422.
Need a row that isn’t part of the loop — a totals line, a “no items” note? Put it outside the table, as a sibling node in the surrounding column. The table is strictly the repeating grid; static lines live next to it.

Cells

Every cell — header or body — has the same shape:
{
  "child": { "type": "text", "value": "{{item.description}}" },
  "border": { "bottom": 1, "color": "grey.lighten2" },
  "padding": 3
}
FieldTypeRequiredNotes
childNodeyesAny node — usually text, but a column or image works too.
border{ bottom, color }noA bottom rule under the cell. See below.
paddingnumber (points)noVertical padding inside the cell.

Cell borders

In v1 a cell border is a bottom rule only — enough for the horizontal lines that make up most tables. Top, left, and right borders aren’t supported yet.
FieldTypeNotes
border.bottomnumber (points)Rule thickness.
border.colorColorDefaults to black; only drawn when bottom is set.
A common pattern: a darker, heavier rule under the header, and a light hairline under each body row.
"header": { "row": [ { "child": { "...": "..." }, "border": { "bottom": 1, "color": "grey.darken1" } } ] },
"body":   { "forEach": "...", "row": [ { "child": { "...": "..." }, "border": { "bottom": 1, "color": "grey.lighten2" } } ] }
Cell counts must match the column count. If columns has three entries, every header.row and body.row must have exactly three cells. A mismatch is a 422 structural error — and the response names the offending row so you can fix it.

Multi-page tables

You don’t manage pagination. When a table’s rows overflow the page, Tipar continues it on the next page automatically, re-drawing the header row at the top. Combine that with a page-number footer for long reports — see the report example.

Next: Images

Embed logos and graphics as base64.