Custom tables in Liquid
Keep managing data in the app, and render it exactly the way you want. All tables and values are accessible in Liquid, anywhere in your theme.
For developers
This guide requires Liquid knowledge. Custom tables require an active app subscription: using the app’s data without one is a breach of our terms.
When to build custom tables
The app blocks render inside a Shadow DOM, which guarantees they look good on every theme but limits styling to the table editor and CSS parts. Building your own Liquid gives you total control over the markup and design, while merchants keep using the app to manage tables, translations and values.
Zero assets
When you render tables with your own Liquid instead of the app blocks, the app doesn’t load any CSS or JavaScript.
Get the table of a product
When a table is manually assigned, it is available from the product metafield:
{%- assign table = product.metafields['app--31615844353--specifications'].table.value -%}
When tables are assigned with conditions, this metafield is empty. Evaluate the rule sets yourself — this snippet mirrors the logic of the app blocks:
{%- liquid
assign table = product.metafields['app--31615844353--specifications'].table.value
if table == blank
for candidate in metaobjects['app--31615844353--tables'].values
assign rule_set = candidate.rule_set.value
assign matched_rules = 0
for rule in rule_set.rules
case rule.relation
when 'equals'
if rule.column == 'collection'
assign collection_id = rule.condition | times: 1
assign matched_collection = product.collections | find: 'id', collection_id
if matched_collection != blank
assign matched_rules = matched_rules | plus: 1
endif
elsif rule.column == 'category'
if product.category.id == rule.condition
assign matched_rules = matched_rules | plus: 1
endif
elsif product[rule.column] == rule.condition
assign matched_rules = matched_rules | plus: 1
endif
when 'not_equals'
if product[rule.column] != rule.condition
assign matched_rules = matched_rules | plus: 1
endif
when 'contains'
if product[rule.column] contains rule.condition
assign matched_rules = matched_rules | plus: 1
endif
endcase
endfor
if rule_set.applied_disjunctively and matched_rules > 0
assign table = candidate
break
elsif matched_rules > 0 and matched_rules == rule_set.rules.size
assign table = candidate
break
endif
endfor
endif
-%}
The app blocks don’t display tables whose status is preview on the live store — they only show them in the theme editor (request.design_mode). Do the same in your code.
List all tables
{%- for table in metaobjects['app--31615844353--tables'].values -%}
{{ table.name.value }}
{%- endfor -%}
Render the groups and attributes
Groups are a JSON field of the metaobject, so access them with .value:
{%- for group in table.groups.value -%}
<h3>{{ group.name }}</h3>
<dl>
{%- for attribute in group.attributes -%}
<dt>
{%- if attribute.icon != blank -%}
{{ images[attribute.icon] | image_url: width: 80 | image_tag: width: 40, alt: '' }}
{%- endif -%}
{{ attribute.name }}
{%- if attribute.tooltip != blank %} <small>{{ attribute.tooltip }}</small>{% endif -%}
</dt>
<dd>{% render 'my-attribute-value', attribute: attribute, product: product %}</dd>
{%- endfor -%}
</dl>
{%- endfor -%}
Available attribute properties: name, type, icon, tooltip, scope, comparable, product_option_name, metafield_reference, metaobject_type, convert_unit, allow_zoom, scale_min, scale_max, show_compare_at_price. See the data model for details.
Resolve attribute values
Product information types
Map the type to the matching product property, for example:
| Type | Liquid |
|---|---|
product_type | product.type |
product_vendor | product.vendor |
product_category | product.category.name |
product_description | product.description |
product_price | product.price or variant.price depending on the scope |
product_sku / product_barcode / product_weight | variant.sku / variant.barcode / variant.weight |
product_collections | product.collections |
product_option | product.options_by_name[attribute.product_option_name] |
Metafield-based types
For all other types, read the metafield referenced by the attribute, on the product or the variant depending on the scope:
{%- liquid
assign namespace = attribute.metafield_reference.namespace
assign key = attribute.metafield_reference.key
if attribute.scope == 'variant'
assign variant = product.selected_or_first_available_variant
assign metafield = variant.metafields[namespace][key]
# Same fallback as the app: use the product metafield when the variant value is empty
if metafield == blank or metafield.value == blank
assign metafield = product.metafields[namespace][key]
endif
else
assign metafield = product.metafields[namespace][key]
endif
-%}
{{ metafield | metafield_tag }}
Metaobjects
When a metafield references a metaobject, metafield_reference also contains metaobject_field_key (the field to display) and metaobject_thumbnail_field_key (an optional image or color field).
For the metaobject_reference attribute type, the metafield stores the handle of the metaobject. Retrieve the entry like this:
{%- liquid
assign handle = product.metafields[attribute.metafield_reference.namespace][attribute.metafield_reference.key].value
assign entry = metaobjects[attribute.metaobject_type][handle]
assign field_key = attribute.metafield_reference.metaobject_field_key
-%}
{{ entry[field_key] }}
Build a comparison table
Compared products are stored on the main product:
{%- assign compared_products = product.metafields['app--31615844353--specifications'].compare_with.value -%}
<table>
<thead>
<tr>
<td></td>
<th scope="col">{{ product.title }}</th>
{%- for compared_product in compared_products -%}
<th scope="col">{{ compared_product.title }}</th>
{%- endfor -%}
</tr>
</thead>
<tbody>
{%- for group in table.groups.value -%}
{%- for attribute in group.attributes -%}
{%- unless attribute.comparable -%}{%- continue -%}{%- endunless -%}
<tr>
<th scope="row">{{ attribute.name }}</th>
<td>{% render 'my-attribute-value', attribute: attribute, product: product %}</td>
{%- for compared_product in compared_products -%}
<td>{% render 'my-attribute-value', attribute: attribute, product: compared_product %}</td>
{%- endfor -%}
</tr>
{%- endfor -%}
{%- endfor -%}
</tbody>
</table>
For variant comparison tables, use the comparable_variants metafield, which contains a list of variants.
Translations
Group names, attribute names and tooltips are translated automatically when you output them from the metaobject: the metaobject is translatable, and Liquid returns the values in the customer’s language.
