Tutorial
How to loop through products in a Shopify collection with Liquid
Render a grid of products from a collection using Liquid's for loop. Here's how to limit the count, access the loop index, handle empty collections, and why you should paginate large collections instead of looping all of them.
Bas Lefeber
Founder, learnshopify.dev · May 9, 2026 · 2 min read
Almost every Shopify section that shows more than one product is a for loop over a collection: featured products, related items, a category grid. The loop itself is three lines; doing it the production way is knowing the limits and the edge cases.
The basic loop
<ul class="product-grid"> {% for product in collection.products %} <li> <a href="{{ product.url }}">{{ product.title }}</a> <span>{{ product.price | money }}</span> </li> {% endfor %}</ul>Limit how many you render
You rarely want every product in a collection on a homepage strip. Cap it with limit:
{% for product in collection.products limit: 4 %} {# ...renders at most 4 products #}{% endfor %}The forloop object
Inside any loop, Liquid gives you a forloop object with useful state — handy for "first" styling, numbering, or stopping early:
{% for product in collection.products limit: 4 %} <li class="{% if forloop.first %}is-featured{% endif %}"> {{ forloop.index }}. {{ product.title }} </li>{% endfor %}forloop.index— current iteration, starting at 1.forloop.index0— same, starting at 0.forloop.first/forloop.last— booleans for the edges.forloop.length— total iterations.
Always handle the empty case
A collection can be empty — newly created, fully sold out, or filtered to nothing. Liquid's for...else gives you a clean empty state:
{% for product in collection.products %} <li>{{ product.title }}</li>{% else %} <p>No products in this collection yet.</p>{% endfor %}Don't loop a whole large collection
Looping collection.products without a limit renders at most 50 products and silently drops the rest — and it's slow. For a full collection page that needs every product, use the paginate tag and loop the paginated set instead. limit is for fixed, small strips; pagination is for full listings.
What AI tools get wrong here
AI suggestions routinely skip the empty state (so an empty collection renders a bare, broken section) and loop large collections without pagination (so products silently vanish past the 50-item cap). Reaching for for...else and knowing when to paginate is the production-grade instinct.
Learn this properly · free lesson
Build a 'Featured Collection' section from the newest products
Build a featured-collection section with a real for loop in the interactive editor — limit, index, and empty state included. Free lesson, no signup wall.
Try this lesson — freeFrequently asked questions
How do I limit the number of products in a Liquid loop?
Add limit to the for tag: {% for product in collection.products limit: 4 %}. It renders at most that many products.
How do I get the loop index in Liquid?
Use the forloop object: forloop.index (starts at 1), forloop.index0 (starts at 0), plus forloop.first, forloop.last, and forloop.length.
How do I show a message when a collection is empty?
Use for...else: put your normal markup in the for block and the empty-state markup after {% else %}. It renders only when the collection has no products.
Why are some products missing from my loop?
Looping collection.products directly returns at most 50 items. For a full collection listing, wrap it in the paginate tag and loop the paginated collection so every product renders across pages.
Keep going in the curriculum