Liquid Tutorial

Product Page Template

Source: Coding with Robby

Building a Complete Product Section

A production product page coordinates media gallery, variant selection, price updates, inventory messaging, and add-to-cart — all in one Online Store 2.0 section. This example shows the Liquid foundation before JavaScript enhancement.

Full product section with media and variants
{%- liquid
  assign current_variant = product.selected_or_first_available_variant
  assign featured_media = current_variant.featured_media | default: product.featured_media
-%}

<div
  class="product"
  id="Product-{{ section.id }}"
  data-section-id="{{ section.id }}"
  data-product-id="{{ product.id }}"
>
  <div class="product__media">
    {% if featured_media %}
      {{ featured_media
        | image_url: width: 1200
        | image_tag: loading: 'eager', alt: product.title, id: 'ProductImage'
      }}
    {% endif %}

    <div class="product__thumbnails">
      {% for media in product.media %}
        <button type="button" data-media-id="{{ media.id }}">
          {{ media | image_url: width: 120 | image_tag: alt: media.alt }}
        </button>
      {% endfor %}
    </div>
  </div>

  <div class="product__info">
    <h1>{{ product.title }}</h1>
    <p class="product__vendor">{{ product.vendor }}</p>

    <div class="product__price" id="Price-{{ section.id }}">
      {% if current_variant.compare_at_price > current_variant.price %}
        <s>{{ current_variant.compare_at_price | money }}</s>
      {% endif %}
      <span>{{ current_variant.price | money }}</span>
    </div>

    {% form 'product', product, id: 'product-form' %}
      {% unless product.has_only_default_variant %}
        {% for option in product.options_with_values %}
          <fieldset>
            <legend>{{ option.name }}</legend>
            {% for value in option.values %}
              <input
                type="radio"
                name="options[{{ option.name | escape }}]"
                value="{{ value | escape }}"
                {% if option.selected_value == value %}checked{% endif %}
              >
              {{ value }}
            {% endfor %}
          </fieldset>
        {% endfor %}
      {% endunless %}

      <input type="hidden" name="id" value="{{ current_variant.id }}">

      <button
        type="submit"
        name="add"
        class="btn"
        {% unless current_variant.available %}disabled{% endunless %}
      >
        {% if current_variant.available %}
          {{ 'products.add_to_cart' | t }}
        {% else %}
          {{ 'products.sold_out' | t }}
        {% endif %}
      </button>
    {% endform %}

    <div class="rte product__description">{{ product.description }}</div>
  </div>
</div>

<script type="application/json" id="ProductJson-{{ product.id }}">
  {{ product | json }}
</script>
  1. options_with_values — OS 2.0 API for variant pickers with selected state
  2. Hidden input name="id" must update when variant changes (JavaScript)
  3. Embed product JSON for client-side variant resolution
  4. loading: 'eager' on hero image — LCP optimization
  5. section.id in element IDs — supports multiple product sections
Note: For 100+ variant products, use a JavaScript variant picker library or Shopify's reference Dawn implementation.