Collections & Products

Almost every storefront template ultimately exists to display collections and products. Both are Liquid objects Shopify populates automatically based on the page being viewed, but products have an extra layer of complexity worth understanding: variants.

The collection object

On a collection page, collection holds that collection's data, and collection.products is the list you loop over:

Liquid templates/collection.liquid
<h1>{{ collection.title }}</h1>
<div class="grid">
{% for product in collection.products %}
  {% render 'product-card', product: product %}
{% endfor %}
</div>

Products and variants

Every product has at least one variant — even a product with no real options (like size or color) still has a single default variant behind the scenes. Price, inventory, and SKU all actually belong to the variant, not the product itself:

Liquid variant data
{{ product.title }}
{{ product.selected_or_first_available_variant.price | money }}
{{ product.selected_or_first_available_variant.sku }}

product.price does work as a shortcut (it resolves to the current or first variant's price), but reaching for the variant explicitly matters the moment a product has real options.

Looping variants and options

Liquid a size selector
<select name="id">
{% for variant in product.variants %}
  <option value="{{ variant.id }}"{% unless variant.available %} disabled{% endunless %}>
    {{ variant.title }} — {{ variant.price | money }}
  </option>
{% endfor %}
</select>

That value="{{ variant.id }}" matters specifically: Shopify's cart and checkout system identifies exactly what to add by variant ID, never by product ID alone.

Filtering and sorting

Liquid isn't meant for heavy data manipulation, but simple filtering is common enough to have dedicated support — the where and sort filters work directly on arrays like collection.products:

Liquid where and sort
{% assign on_sale = collection.products | where: "compare_at_price_max", 0 %}
{% assign cheapest_first = collection.products | sort: "price" %}

Pagination

A collection can hold thousands of products, so Liquid provides {% paginate %} to split a long list into pages instead of rendering everything at once:

Liquid paginating a collection
{% paginate collection.products by 24 %}
  {% for product in collection.products %}
    {% render 'product-card', product: product %}
  {% endfor %}
  {{ paginate | default_pagination }}
{% endpaginate %}

default_pagination renders a ready-made Previous/Next control automatically, built from the pagination state paginate exposes (current page, total pages, and links to each).

Product recommendations

Shopify can generate "related products" suggestions automatically using its own recommendation model, available through a dedicated Liquid object rather than something you compute yourself:

Liquid product recommendations
{% if recommendations.performed and recommendations.products_count > 0 %}
  {% for product in recommendations.products %}
    {% render 'product-card', product: product %}
  {% endfor %}
{% endif %}
Note: a product's images live in product.images, but each variant can also have its own specific image (useful for color swatches) accessible as variant.image — falling back to the product's default image when a variant has none of its own.