Conditional Visibility & Layout Collapse
How props.visible and expressions.visibleWhen hide a node, when to use each one, and how a layout_group recalculates column widths when a child is hidden.
Nodes in a REFRACT template can be hidden two ways. Both remove the node from the document. They differ in when the decision is made: props.visible is fixed at authoring time, expressions.visibleWhen is evaluated against the document's data at render time. Either mechanism works on any node type.
Whether the surrounding layout adapts to fill the gap is a separate question, covered in Layout Collapse below - it depends on where the node lives, not on which mechanism hid it.
The Two Mechanisms
props.visible
A literal value in the template. The node is hidden or shown the same way for every document rendered from that template.
{
"id": "draft_watermark_note",
"type": "note_block",
"props": {
"text": "Draft copy, not for distribution.",
"visible": false
}
}Use this when a node is switched off at authoring time - for example, a block kept in the template for reference but not currently wanted.
expressions.visibleWhen
A rule evaluated against the document's own data. The same template shows the node for one document and hides it for another.
{
"id": "consent_record_card",
"type": "card",
"props": {
"title": "Consent Record",
"columns": "1"
},
"expressions": {
"visibleWhen": "signing.isMultiSigner !== true"
}
}Here the card renders when the payload's signing.isMultiSigner is anything other than true, and is hidden when it is true.
Use this whenever the decision depends on the data. This is the same expressions.visibleWhen mechanism documented per-node-type on Section, Layout Group, Table, and Spacer / Separator - this page is the syntax and behavior reference all of those point back to.
Layout Collapse
Hiding a node removes it from the document - that part is the same everywhere, for any node type. What happens to the space it occupied depends on where the node lives.
Outside a Layout Group, that's the whole story: the node simply does not render, and nothing shifts to fill the gap.
Inside a Layout Group, it's different. A Layout Group divides horizontal space between its children, and a hidden child is excluded from that calculation - the remaining children share the full width, so no empty column is left behind. This reflow is specific to the Layout Group; it is not something visibleWhen or props.visible do on their own.
Given a two-column group:
{
"id": "verification_group",
"type": "layout_group",
"props": {
"columns": [
{ "width": "star" },
{ "width": "star" }
]
},
"children": [
{ "id": "verification_card", "type": "card", "props": { "title": "Signing and Verification" } },
{
"id": "consent_record_card",
"type": "card",
"props": { "title": "Consent Record" },
"expressions": { "visibleWhen": "signing.isMultiSigner !== true" }
}
]
}With isMultiSigner false, both cards render side by side, each taking half the width.
With isMultiSigner true, the consent card is hidden and the verification card takes the full width on its own.
Omitting width entirely has the same effect as "star" - useful shorthand for the common case of an even split.
The same logic extends to fixed widths: a group with one fixed-width column and one star column gives the star column whatever space remains after the fixed column is subtracted. If the fixed column is hidden, the star column takes the full width instead.
This is column collapse within a group, not the group's own visibility
A layout_group's own expressions.visibleWhen (documented on the Layout Group page) hides the entire group, all columns included. The collapse described here is different: a child of the group is hidden via its own visibleWhen, and the group reflows the remaining children to fill the freed space.
Writing a visibleWhen Expression
Expressions are read against the document's data using dotted paths, and support the usual comparison and logical operators.
"visibleWhen": "invoice.total > 0"
"visibleWhen": "customer.country === 'US'"
"visibleWhen": "signing.isMultiSigner !== true"
"visibleWhen": "order.status === 'approved' && order.lineItems.length > 0"A path that does not exist in the data resolves to undefined, so "visibleWhen": "customer.vatNumber" hides the node when the field is absent, empty, or zero.
More Forms
"visibleWhen": "!customer.isInternal"Negation. Reads better than comparing to false, and treats an absent field the same way any other falsy value is treated.
"visibleWhen": "settings.enabled == true"Loose equality. Converts types before comparing - the right tool when a field's type varies depending on how the payload was produced. See Comparisons Are Strict below for why === doesn't fit that case.
"visibleWhen": "order.total > 1000 ? true : order.isPriority"Ternary is supported.
Comparisons Are Strict
=== and !== do not convert types
A field arriving as the string "true" is not equal to the boolean true.
"visibleWhen": "settings.enabled === true" matches boolean true only
"visibleWhen": "settings.enabled === 'true'" matches the string only
"visibleWhen": "settings.enabled == true" matches either, via type coercion
"visibleWhen": "settings.enabled" matches either, and any other truthy valueIf a field's type varies depending on how the payload was produced, use == or the bare truthy form, or normalize the value before it reaches the template.
What's Not Supported
Member access works - foo.bar and items.length both resolve. Method and function calls do not: items.includes('x') and anything shaped like it will not work. This is usually the first thing people try, so it's worth stating plainly.
Full supported grammar
Literals, identifiers, member access (foo.bar), unary ! + -, binary comparison (> < >= <= == != === !==) and arithmetic (+ - * / %), logical && and ||, and the ternary ? :. No array literals, no function or method calls.
Arithmetic works, but a visibleWhen that needs it is usually a sign the payload should have carried the computed value instead - it isn't shown as a recommended pattern above.
Which to Use
Use props.visible when the answer is the same for every document.
Use expressions.visibleWhen when the answer depends on the data.
Do not use both on the same node
Two conditions governing one decision will eventually disagree, and which one takes effect is not something a template author should have to reason about.
A Worked Example
The eSign Certificate of Completion presents consent two ways depending on how many people signed.
A single signer's consent is the document's consent, so it renders once in a document-level Consent Record card:
{
"id": "consent_record_card",
"type": "card",
"props": { "title": "Consent Record", "columns": "1" },
"expressions": { "visibleWhen": "signing.isMultiSigner !== true" }
}With several signers, each consented separately, so one document-level card cannot represent them. The card is hidden and each signer's own consent renders inside their evidence block instead.
The layout follows automatically. On a single-signer certificate the Consent Record sits beside Signing and Verification, each at half width. On a multi-signer certificate the card is hidden and Signing and Verification takes the full width, with no gap where the consent card would have been.
Conditional Styles & Advanced Formatting
Guide to the conditional styling system and advanced formatting options available for cards, tables, hierarchical tables, and note blocks.
Image Support Guide
Comprehensive guide to image rendering options in REFRACT PDF, including standalone images, image grids, and inline images in tables and cards. Supports both SharePoint asset paths and Base64 data URIs.