Control Flow: if/elsif/for/case
Master conditional statements and loops in Liquid to build dynamic Shopify themes that respond to data and user context.
Control flow is what makes your themes dynamic. Instead of static HTML, you can show different content based on conditions, loop through products, and respond to user actions. Let’s master the essential control flow tools in Liquid.
Conditional Statements
The if Statement
The most basic conditional checks if something is true:
{% if product.available %} <button>Add to Cart</button>{% endif %}Adding else
Provide an alternative when the condition is false:
{% if product.available %} <button>Add to Cart</button>{% else %} <button disabled>Sold Out</button>{% endif %}Multiple Conditions with elsif
Check several conditions in order:
{% if product.compare_at_price > product.price %} <span class="badge sale">On Sale!</span>{% elsif product.available == false %} <span class="badge soldout">Sold Out</span>{% elsif product.tags contains "new" %} <span class="badge new">New Arrival</span>{% else %} {# No badge #}{% endif %}Important: Liquid uses elsif, not elseif or else if.
The unless Statement
unless is the opposite of if. It runs when the condition is false:
{% unless product.available %} <p class="warning">This product is currently unavailable.</p>{% endunless %}This is equivalent to:
{% if product.available == false %} <p class="warning">This product is currently unavailable.</p>{% endif %}Use unless when it reads more naturally. “Unless the product is available” often flows better than “if the product is not available.”
The case Statement
When comparing one value against multiple options, case is cleaner than many elsif statements:
{% case product.type %} {% when "Shirt" %} {% render 'size-guide-shirt' %} {% when "Pants" %} {% render 'size-guide-pants' %} {% when "Shoes" %} {% render 'size-guide-shoes' %} {% else %} {% render 'size-guide-general' %}{% endcase %}You can match multiple values in one when:
{% case shipping_method %} {% when "express", "overnight" %} <p>Your order will arrive within 1-2 days.</p> {% when "standard" %} <p>Your order will arrive within 5-7 days.</p> {% else %} <p>Shipping time varies.</p>{% endcase %}Comparison Operators
Equality and Inequality
{% if product.type == "Shirt" %} {# Equal to #}{% if product.type != "Shirt" %} {# Not equal to #}Numeric Comparisons
{% if product.price > 5000 %} {# Greater than #}{% if product.price >= 5000 %} {# Greater than or equal #}{% if product.price < 5000 %} {# Less than #}{% if product.price <= 5000 %} {# Less than or equal #}Remember: Shopify stores prices in cents, so 5000 means $50.00.
The contains Operator
Check if a string contains a substring or an array contains an item:
{# String contains #}{% if product.title contains "Limited" %} <span class="badge">Limited Edition</span>{% endif %}
{# Array contains #}{% if product.tags contains "sale" %} <span class="badge">Sale</span>{% endif %}Logical Operators
Combine conditions with and and or:
{# Both must be true #}{% if product.available and product.price < 5000 %} <p>In stock and under $50!</p>{% endif %}
{# Either can be true #}{% if product.type == "Shirt" or product.type == "Pants" %} {% render 'clothing-care-instructions' %}{% endif %}Important: Liquid doesn’t support parentheses for grouping conditions. If you need complex logic, use nested conditionals or assign intermediate variables:
{# Instead of: (a and b) or c #}{% assign is_discounted = false %}{% if product.compare_at_price > product.price and product.available %} {% assign is_discounted = true %}{% endif %}
{% if is_discounted or product.tags contains "sale" %} <span class="sale-badge">Sale!</span>{% endif %}Truthy and Falsy Values
Understanding what Liquid considers “true” or “false” is crucial:
Falsy Values (evaluate to false)
{% if false %} {# false #}{% if nil %} {# nil/null #}{% if empty %} {# empty keyword #}Truthy Values (evaluate to true)
Everything else is truthy, including some surprises:
{% if true %} {# true #}{% if "string" %} {# any string #}{% if "" %} {# empty string is TRUTHY! #}{% if 0 %} {# zero is TRUTHY! #}{% if product %} {# objects #}{% if collection.products %} {# arrays (even empty ones) #}Checking for Empty Values
Use blank to check for nil, false, or empty strings/arrays:
{% if product.description == blank %} <p>No description available.</p>{% endif %}
{# Same as: #}{% unless product.description != blank %} <p>No description available.</p>{% endunless %}The blank check is safer than checking for an empty string because it handles nil too:
{# Problematic: fails if description is nil #}{% if product.description == "" %}
{# Safe: handles nil, "", and empty #}{% if product.description == blank %}Loops with for
Basic For Loop
Iterate over arrays:
{% for product in collection.products %} <div class="product-card"> <h3>{{ product.title }}</h3> <p>{{ product.price | money }}</p> </div>{% endfor %}The forloop Object
Inside every loop, you have access to the forloop object:
{% for product in collection.products %} {{ forloop.index }} {# 1, 2, 3, 4... (1-based) #} {{ forloop.index0 }} {# 0, 1, 2, 3... (0-based) #} {{ forloop.rindex }} {# 4, 3, 2, 1... (reverse count) #} {{ forloop.rindex0 }} {# 3, 2, 1, 0... #} {{ forloop.first }} {# true on first iteration #} {{ forloop.last }} {# true on last iteration #} {{ forloop.length }} {# total number of items #}{% endfor %}Practical uses:
<ul> {% for product in collection.products %} <li class="{% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %}"> {{ forloop.index }}. {{ product.title }} </li> {% endfor %}</ul>Loop Parameters
Control how many items to loop through:
{# Limit to first 4 items #}{% for product in collection.products limit: 4 %} {{ product.title }}{% endfor %}
{# Skip the first 2 items #}{% for product in collection.products offset: 2 %} {{ product.title }}{% endfor %}
{# Combine: skip 2, then show 4 #}{% for product in collection.products offset: 2 limit: 4 %} {{ product.title }}{% endfor %}
{# Reverse order #}{% for product in collection.products reversed %} {{ product.title }}{% endfor %}The else Clause in Loops
Handle empty arrays:
{% for product in collection.products %} <div class="product-card">{{ product.title }}</div>{% else %} <p>No products found in this collection.</p>{% endfor %}Breaking and Continuing
Control loop flow:
{# Stop the loop early #}{% for product in collection.products %} {% if product.handle == "special" %} {% break %} {% endif %} {{ product.title }}{% endfor %}
{# Skip this iteration #}{% for product in collection.products %} {% if product.available == false %} {% continue %} {% endif %} {{ product.title }} - In Stock{% endfor %}Ranges
Loop through a sequence of numbers:
{# Static range #}{% for i in (1..5) %} {{ i }}{% endfor %}{# Output: 1 2 3 4 5 #}
{# Dynamic range #}{% assign max = 10 %}{% for i in (1..max) %} <option value="{{ i }}">{{ i }}</option>{% endfor %}
{# Quantity selector #}<select name="quantity"> {% for i in (1..10) %} <option value="{{ i }}">{{ i }}</option> {% endfor %}</select>Nested Loops
When nesting loops, use forloop.parentloop to access the outer loop:
{% for collection in collections %} <h2>{{ collection.title }}</h2> <ul> {% for product in collection.products limit: 3 %} <li> Collection {{ forloop.parentloop.index }}, Product {{ forloop.index }}: {{ product.title }} </li> {% endfor %} </ul>{% endfor %}The cycle Tag
Alternate between values on each iteration:
{% for product in collection.products %} <div class="product-card {% cycle 'odd', 'even' %}"> {{ product.title }} </div>{% endfor %}Output:
<div class="product-card odd">Product 1</div><div class="product-card even">Product 2</div><div class="product-card odd">Product 3</div><div class="product-card even">Product 4</div>Use named cycles when you have multiple cycles in nested loops:
{% for row in (1..3) %} {% cycle 'row': 'row-a', 'row-b' %} {% for col in (1..3) %} {% cycle 'col': 'col-1', 'col-2', 'col-3' %} {% endfor %}{% endfor %}The tablerow Tag
Generate HTML table rows automatically:
<table> {% tablerow product in collection.products cols: 3 %} {{ product.title }} {% endtablerow %}</table>Output:
<table> <tr class="row1"> <td class="col1">Product 1</td> <td class="col2">Product 2</td> <td class="col3">Product 3</td> </tr> <tr class="row2"> <td class="col1">Product 4</td> ... </tr></table>Practical Examples
Sale Badge Logic
{% assign on_sale = false %}{% if product.compare_at_price > product.price %} {% assign on_sale = true %} {% assign savings = product.compare_at_price | minus: product.price %} {% assign savings_percent = savings | times: 100.0 | divided_by: product.compare_at_price | round %}{% endif %}
{% if on_sale %} <span class="sale-badge"> Save {{ savings_percent }}% </span>{% endif %}Responsive Product Grid
<div class="product-grid"> {% for product in collection.products %} <article class="product-card {% if forloop.index <= 2 %}featured{% endif %} {% cycle 'position': 'left', 'center', 'right' %}">
<a href="{{ product.url }}"> {% if product.featured_image %} <img src="{{ product.featured_image | image_url: width: 400 }}" alt="{{ product.featured_image.alt | escape }}" loading="{% if forloop.index <= 4 %}eager{% else %}lazy{% endif %}"> {% endif %}
<h3>{{ product.title }}</h3>
<p class="price"> {{ product.price | money }} {% if product.compare_at_price > product.price %} <del>{{ product.compare_at_price | money }}</del> {% endif %} </p> </a> </article> {% else %} <p class="no-products">No products available.</p> {% endfor %}</div>Variant Selector
{% if product.has_only_default_variant == false %} {% for option in product.options_with_values %} <div class="variant-option"> <label>{{ option.name }}</label> <select name="options[{{ option.name }}]"> {% for value in option.values %} <option value="{{ value }}" {% if option.selected_value == value %}selected{% endif %}> {{ value }} </option> {% endfor %} </select> </div> {% endfor %}{% endif %}Practice Exercise
Build a navigation menu that:
- Loops through menu items
- Shows a dropdown if the item has children
- Highlights the current page
- Limits the main menu to 6 items
<nav class="main-nav"> <ul class="nav-list"> {% for link in linklists.main-menu.links limit: 6 %} <li class="nav-item {% if link.active %}is-active{% endif %} {% if link.links.size > 0 %}has-dropdown{% endif %}"> <a href="{{ link.url }}">{{ link.title }}</a>
{% if link.links.size > 0 %} <ul class="dropdown"> {% for child in link.links %} <li class="dropdown-item {% if child.active %}is-active{% endif %}"> <a href="{{ child.url }}">{{ child.title }}</a> </li> {% endfor %} </ul> {% endif %} </li> {% endfor %} </ul></nav>Try it live: Test conditionals, loops, and
forloopvariables in our Liquid Playground with pre-loaded product and collection data.
Key Takeaways
if/elsif/elsehandle conditional renderingunlessis cleaner thanif notfor negative conditionscase/whenis best for matching one value against many optionsforloops iterate over arrays with access toforlooppropertieslimitandoffsetcontrol loop boundariesbreakandcontinuegive fine-grained loop controlblankis safer than empty string checks for nil handling- Empty strings and zero are truthy in Liquid (common gotcha!)
What’s Next?
Now that you can control program flow, the next lesson covers Snippets and Render Patterns for creating reusable, maintainable components in your theme.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...