Template Components

Card

The Card node in REFRACT PDF. Renders a structured key-value block with configurable variants, border styles, label layouts, and field definitions. Ideal for contact details, order summaries, address blocks, and grouped metadata.

Overview

A Card (card) is a self-contained block that displays a set of labelled fields as a structured table. Unlike a section (which lays out child nodes vertically), a card owns its field definitions directly through a fields array, making it more portable and easier to compose inside layout groups.

Four variants give the card different visual treatments:

VariantBest used for
defaultGeneral key-value information (customer details, order metadata)
summaryTotals and KPI callouts - each field becomes a mini labeled tile
addressPostal or delivery addresses - values stacked as a single text block, no labels
groupedNested payload objects - renders child cards recursively

Title & Header

PropertyTypeDefaultDescription
titlestring-Card heading. Supports {{FIELDNAME}} tokens.
subtitlestring-Optional secondary line rendered beneath the title in a smaller muted font.
titleIconstring-Decorative icon key placed next to the title (e.g. "star", "package", "list").
accentColorcolortheme.primaryColor applied to the title text, icon accent pipe, and -- when showHeader is off -- the inline title.
showHeaderbooleanfalseWhen true, renders a filled header bar (same style as a section header). When false, the title is rendered inline as an accent-colored heading inside the card body.
headerFillColorcolortheme.primaryBackground color of the header bar. Only relevant when showHeader is true.
headerBorderBottomnumber0.5 when a header fill color is active, otherwise 0Width (pt) of the border line drawn below the header bar.

Title-less cards

Set showHeader: false and omit title entirely to produce a title-less card - useful for compact summary tiles inside a layout group where the section already provides a heading.


Data Binding

PropertyTypeDefaultDescription
dataPathstring-Dot-path into the JSON payload used to scope field-name suggestions in the Designer. Example: "customer" makes the Designer suggest fields from payload.customer.

All field value expressions resolve against the full JSON payload root - dataPath does not restrict which payload fields can be referenced. Setting dataPath tells the Designer which object in your payload to use when suggesting field names, but you can reference any payload path in a value string regardless.


Variant

PropertyTypeDefaultDescription
variant"default" | "summary" | "address" | "grouped""default"Controls the visual treatment of the card body. See Variant Details below.

Field Layout

PropertyTypeDefaultDescription
labelPosition"left" | "top""left""left" - label and value sit side-by-side in a two-column row. "top" - label renders above the value in a single column.
labelWidthnumber110Width (pt) of the label column when labelPosition is "left". Ignored for "top" layout.
fieldsTagAs"table" | "list""table"How the fields are tagged when Section 508 tagging is on, which is what a screen reader announces. "table": a key/value table, one row per field, the label a row header and the value a cell, wherever the labels sit and however many columns. "list": a list, one item per field, the label the item's label and the value its body. Most cards read best as a table; set "list" for a short summary that reads as a list, such as a status card. A card whose fields have no labels is tagged as paragraphs either way. Only the tags change; the card looks the same. Any other value is ignored, and template validation warns about it.
valueAlign"left" | "right" | "center""left"Horizontal alignment of value text within its cell.
columns1 | 2 | 31Splits the fields array into N side-by-side columns inside the card body. 0 and other falsy values fall back to 1; there is no auto-fill mode.
fieldGapnumber8 (10 for summary, 2 for address, 8 for grouped)Gap (pt) between internal field columns. Also used as the row spacing between mini-tiles in summary variant.
labelBoldbooleantrueApply bold to all label cells.
valueBoldbooleanfalseApply bold to all value cells.
valueAllCapsbooleanfalseUppercase all value text. Static values are transformed at build time; {{token}} values are flagged for runtime transformation.
emphasizeLastRowbooleanfalseRenders the last field row with bold text and a slightly larger font size -- useful for "Total" rows in a summary card.
showDividerbooleansame as emphasizeLastRowDraws a horizontal separator line immediately above the last field row when there are two or more fields. Set explicitly to override the default.

Border & Background

PropertyTypeDefaultDescription
borderStyle"full" | "minimal" | "none""full" ("minimal" for address variant)Outer card border. "full" -- full rectangle on all 4 sides. "minimal" -- bottom line only. "none" -- no outer border.
borderColorcolortheme.borderColorColor of the outer card border.
innerBorderStyle"full" | "minimal" | "none""minimal"Divider lines between field rows. "full" -- full-width row separators. "minimal" -- bottom-only line on each row. "none" -- no row dividers.
innerBorderColorcolortheme.borderColorColor of the inner row dividers.
backgroundColorcolor(transparent)Card body fill color. Useful for highlighted summary blocks.
radiusnumber0Corner radius (pt) applied to the outer card border. Range: 0--16.

Spacing

PropertyTypeDefaultDescription
paddingnumber12 (10 for summary/address, 0 for grouped)Inner padding (pt) applied to the card content area.

marginTop, marginBottom and width are not read by the card renderer - the renderer's card case has no code path that consumes them. To control spacing around a card, wrap it in a layout_group or section, whose own marginTop/marginBottom props are consumed. To size a card inside a layout_group, use that group's columns[].width, not a width prop on the card itself.


Variant Details

Default

The standard label-value block. Each field renders as a two-column row (label left, value right) or a single stacked column when labelPosition: "top". Rows are separated by innerBorderStyle lines.

{
  "type": "card",
  "props": {
    "title": "Customer",
    "variant": "default",
    "showHeader": true,
    "labelPosition": "left",
    "labelWidth": 110,
    "valueAlign": "left",
    "borderStyle": "full",
    "innerBorderStyle": "minimal",
    "fields": [
      { "key": "name",    "label": "Name",    "value": "{{customer.name}}" },
      { "key": "email",   "label": "Email",   "value": "{{customer.email}}" },
      { "key": "phone",   "label": "Phone",   "value": "{{customer.phone}}" }
    ]
  }
}

Summary

Each field becomes a mini tile - a small table cell with the label displayed in a filled header bar (accentColor background, contrast text) and the value centered below. Tiles are arranged in rows of up to columns tiles each (1, 2 or 3; columns: 0 falls back to 1, it does not auto-fill).

Use emphasizeLastRow: true and showDivider: true to visually separate a total row from sub-totals.

{
  "type": "card",
  "props": {
    "title": "Order Totals",
    "titleIcon": "star",
    "variant": "summary",
    "showHeader": false,
    "columns": 1,
    "valueAlign": "right",
    "emphasizeLastRow": true,
    "showDivider": true,
    "fields": [
      { "key": "subtotal", "label": "Subtotal",  "value": "{{subtotal}}",  "format": "currency" },
      { "key": "tax",      "label": "GST (10%)", "value": "{{tax}}",       "format": "currency" },
      { "key": "total",    "label": "Total",     "value": "{{total}}",     "format": "currency" }
    ]
  }
}

Address

Values are rendered as a single multiline text block - one line per field, labels omitted. Ideal for postal addresses, delivery notes, or any block where the formatting itself conveys structure.

The outer borderStyle defaults to "minimal" for this variant (bottom-line only). Set borderStyle: "none" for a borderless address block.

{
  "type": "card",
  "props": {
    "title": "Deliver To",
    "variant": "address",
    "showHeader": false,
    "borderStyle": "none",
    "fields": [
      { "key": "line_0", "label": "Name",           "value": "{{deliveryName}}" },
      { "key": "line_1", "label": "Street",         "value": "{{deliveryStreet}}" },
      { "key": "line_2", "label": "City/State/Zip", "value": "{{deliveryCity}}, {{deliveryState}} {{deliveryPostcode}}" }
    ]
  }
}

Grouped

Renders a nested payload object as a stack of sub-cards. Instead of a flat fields array, a grouped card holds a children array - each child is typically a default or address card. Use this variant when your payload contains nested objects (for example, a shipTo object that itself contains a contact sub-object) and you want each level to appear as a distinct, titled card block.


Fields

The fields array defines the rendered rows (or tiles, for summary). Fields are rendered in declaration order.

Field Properties

PropertyTypeRequiredDescription
keystringYesUnique identifier for the field within this card.
labelstringYesDisplay label. Shown as-is in default variant; used as tile heading in summary. Omitted in address.
valuestringYesValue text. Supports {{fieldName}} and {{fieldName|format}} tokens.
formatstringNoFormat hint applied to {{token}} values. See Field Formats.
hiddenbooleanNoWhen true, the field is omitted from the rendered output. Useful for toggling fields without removing them from the schema.
hideWhenEmptybooleanNoWhen true, the field is left out if its value is empty: every {{token}} in value resolves to a missing path, null, or blank text. 0 and false are values, so those fields still show. See Hiding a field when its value is empty.
styleobjectNoPer-field style overrides: { fontSize, color }.
span"full"NoForce the field to render at full card width, below the multi-column field layout. Useful for long text or signature-style lines.

Field Formats

ValueEffect
textPlain string -- no transformation (default).
numberNumeric with locale-aware thousands separator.
currencyLocale-formatted currency value (e.g. $1,234.56).
dateShort date string (e.g. 31/03/2026).
datetimeDate and time (e.g. 31/03/2026 14:22).
booleanRenders true/false as Yes/No.
percentageAppends % and formats as a number.

Formats can also be embedded inline in the value string using the pipe syntax: "{{amount|currency}}".

Connecting tokens to your payload

{{FIELDNAME}} tokens are resolved against the full JSON payload at render time. Use dot-notation for nested fields: {{order.customer.name}} reads payload.order.customer.name. Field names containing dots or spaces are also supported using the greedy longest-match resolver - for example {{Header.No.}} resolves the literal key "No." on Header.


Visibility

To hide an entire card, set visible: false in its props. This is a generic check applied before any node is built, and it works on every node type including card.

A top-level hidden prop on the card itself is not read. hidden only has an effect inside individual fields[] entries (see Field Properties), where it omits that one field from the rendered output.

Hiding a field when its value is empty

hidden is decided once, in the template. To decide per document instead, set hideWhenEmpty: true on the field. When every {{token}} in the field's value resolves to a missing path, null, or text that is empty or only spaces, the whole field is left out, label included, rather than showing a label with nothing beside it. This applies in the default, address and summary variants.

  • 0 and false are values, not empty ones, so a field whose token resolves to either still shows.
  • A value that is literal text with no token is never hidden.
  • If value holds more than one token, the field is hidden only when all of them are empty. "{{city}}, {{state}}" still shows when either city or state has a value, and is left out when both are empty.
{
  "type": "card",
  "props": {
    "title": "Customer",
    "fields": [
      { "key": "name",  "label": "Name",        "value": "{{customer.name}}" },
      { "key": "vat",   "label": "VAT Number",  "value": "{{customer.vatNumber}}",  "hideWhenEmpty": true },
      { "key": "terms", "label": "Credit Days", "value": "{{customer.creditDays}}", "hideWhenEmpty": true }
    ]
  }
}

With "vatNumber": "" and "creditDays": 0 in the payload, the card shows Name and Credit Days (as 0) and leaves out VAT Number.


Examples

Info card -- two columns, top label

{
  "type": "card",
  "props": {
    "title": "Order Details",
    "titleIcon": "package",
    "variant": "default",
    "showHeader": true,
    "labelPosition": "top",
    "columns": 2,
    "fieldGap": 4,
    "borderStyle": "full",
    "innerBorderStyle": "none",
    "padding": 10,
    "fields": [
      { "key": "orderId",    "label": "Order Number", "value": "{{orderId}}" },
      { "key": "orderDate",  "label": "Date",         "value": "{{orderDate}}",  "format": "date" },
      { "key": "reference",  "label": "Reference",    "value": "{{reference}}" },
      { "key": "dueDate",    "label": "Due Date",     "value": "{{dueDate}}",    "format": "date" }
    ]
  }
}

Summary card -- totals block on the right

Pair this card with a table on the left inside a layout_group:

{
  "type": "card",
  "props": {
    "title": "Invoice Totals",
    "variant": "summary",
    "showHeader": false,
    "accentColor": "theme.primary",
    "columns": 1,
    "valueAlign": "right",
    "labelBold": true,
    "emphasizeLastRow": true,
    "showDivider": true,
    "borderStyle": "full",
    "innerBorderStyle": "full",
    "padding": 10,
    "fields": [
      { "key": "subtotal", "label": "Subtotal",   "value": "{{subtotal}}",   "format": "currency" },
      { "key": "discount", "label": "Discount",   "value": "{{discount}}",   "format": "currency" },
      { "key": "tax",      "label": "GST (10%)",  "value": "{{tax}}",        "format": "currency" },
      { "key": "total",    "label": "Total Due",  "value": "{{totalDue}}",   "format": "currency" }
    ]
  }
}

Address card -- borderless

{
  "type": "card",
  "props": {
    "title": "Ship To",
    "variant": "address",
    "showHeader": false,
    "borderStyle": "none",
    "accentColor": "theme.primary",
    "padding": 0,
    "fields": [
      { "key": "company", "label": "Company", "value": "{{shipTo.company}}" },
      { "key": "street",  "label": "Street",  "value": "{{shipTo.street}}" },
      { "key": "suburb",  "label": "City",    "value": "{{shipTo.city}}, {{shipTo.state}}  {{shipTo.postcode}}" },
      { "key": "country", "label": "Country", "value": "{{shipTo.country}}" }
    ]
  }
}

Placing Cards in Layout Groups

Cards placed inside a layout_group fill the column width computed by the group. Pin a card to a fixed pt size by setting width on the group's columns[] entry, not on the card itself - a width prop on the card is not read:

{
  "type": "layout_group",
  "props": { "columns": [{ "width": "star" }, { "width": 160 }], "columnGap": 12 },
  "children": [
    { "type": "table", "props": { "...": "..." } },
    {
      "type": "card",
      "props": {
        "variant": "summary"
      }
    }
  ]
}

Equal-height borders in layout groups

When a card is placed inside a layout_group, its borderStyle is promoted to the cell level so all cells in the same row share equal-height borders. Cards and sections placed side-by-side will automatically align their border lines.


  • Section - child-node-based layout block; use cards when you want self-contained field ownership
  • Layout Group - multi-column container for placing cards side-by-side
  • Table - for array-backed tabular data
  • Template Settings - theme tokens referenced by accentColor, borderColor, and headerFillColor

On this page