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:
| Variant | Best used for |
|---|---|
default | General key-value information (customer details, order metadata) |
summary | Totals and KPI callouts - each field becomes a mini labeled tile |
address | Postal or delivery addresses - values stacked as a single text block, no labels |
grouped | Nested payload objects - renders child cards recursively |
Title & Header
| Property | Type | Default | Description |
|---|---|---|---|
title | string | - | Card heading. Supports {{FIELDNAME}} tokens. |
subtitle | string | - | Optional secondary line rendered beneath the title in a smaller muted font. |
titleIcon | string | - | Decorative icon key placed next to the title (e.g. "star", "package", "list"). |
accentColor | color | theme.primary | Color applied to the title text, icon accent pipe, and -- when showHeader is off -- the inline title. |
showHeader | boolean | false | When 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. |
headerFillColor | color | theme.primary | Background color of the header bar. Only relevant when showHeader is true. |
headerBorderBottom | number | 0.5 when a header fill color is active, otherwise 0 | Width (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
| Property | Type | Default | Description |
|---|---|---|---|
dataPath | string | - | 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
| Property | Type | Default | Description |
|---|---|---|---|
variant | "default" | "summary" | "address" | "grouped" | "default" | Controls the visual treatment of the card body. See Variant Details below. |
Field Layout
| Property | Type | Default | Description |
|---|---|---|---|
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. |
labelWidth | number | 110 | Width (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. |
columns | 1 | 2 | 3 | 1 | Splits 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. |
fieldGap | number | 8 (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. |
labelBold | boolean | true | Apply bold to all label cells. |
valueBold | boolean | false | Apply bold to all value cells. |
valueAllCaps | boolean | false | Uppercase all value text. Static values are transformed at build time; {{token}} values are flagged for runtime transformation. |
emphasizeLastRow | boolean | false | Renders the last field row with bold text and a slightly larger font size -- useful for "Total" rows in a summary card. |
showDivider | boolean | same as emphasizeLastRow | Draws 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
| Property | Type | Default | Description |
|---|---|---|---|
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. |
borderColor | color | theme.borderColor | Color 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. |
innerBorderColor | color | theme.borderColor | Color of the inner row dividers. |
backgroundColor | color | (transparent) | Card body fill color. Useful for highlighted summary blocks. |
radius | number | 0 | Corner radius (pt) applied to the outer card border. Range: 0--16. |
Spacing
| Property | Type | Default | Description |
|---|---|---|---|
padding | number | 12 (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
| Property | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique identifier for the field within this card. |
label | string | Yes | Display label. Shown as-is in default variant; used as tile heading in summary. Omitted in address. |
value | string | Yes | Value text. Supports {{fieldName}} and {{fieldName|format}} tokens. |
format | string | No | Format hint applied to {{token}} values. See Field Formats. |
hidden | boolean | No | When true, the field is omitted from the rendered output. Useful for toggling fields without removing them from the schema. |
hideWhenEmpty | boolean | No | When 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. |
style | object | No | Per-field style overrides: { fontSize, color }. |
span | "full" | No | Force the field to render at full card width, below the multi-column field layout. Useful for long text or signature-style lines. |
Field Formats
| Value | Effect |
|---|---|
text | Plain string -- no transformation (default). |
number | Numeric with locale-aware thousands separator. |
currency | Locale-formatted currency value (e.g. $1,234.56). |
date | Short date string (e.g. 31/03/2026). |
datetime | Date and time (e.g. 31/03/2026 14:22). |
boolean | Renders true/false as Yes/No. |
percentage | Appends % 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.
0andfalseare values, not empty ones, so a field whose token resolves to either still shows.- A
valuethat is literal text with no token is never hidden. - If
valueholds more than one token, the field is hidden only when all of them are empty."{{city}}, {{state}}"still shows when eithercityorstatehas 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.
Related Pages
- 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, andheaderFillColor
Section
The primary content container in REFRACT PDF. Sections group related fields and controls, provide titled regions with optional accent pipes and icons, and support a full box model for borders, fills, and header rows.
Bento Card
A visually-rich feature showcase card. The Bento Card renders a tiered information block with an icon, badge, title, hero metric, feature list, and call-to-action. Ideal for plan comparisons, service tiers, product offerings, and KPI summaries.