DocsDevelopers

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:

Liquid
{%- 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
{%- 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

Liquid
{%- 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:

Liquid
{%- 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:

TypeLiquid
product_typeproduct.type
product_vendorproduct.vendor
product_categoryproduct.category.name
product_descriptionproduct.description
product_priceproduct.price or variant.price depending on the scope
product_sku / product_barcode / product_weightvariant.sku / variant.barcode / variant.weight
product_collectionsproduct.collections
product_optionproduct.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
{%- 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
{%- 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:

Liquid
{%- 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.

Still stuck?

Our support team answers every email, usually within one business day.

Contact support