Liquid Fundamentals for Theme Developers Beginner 12 min read

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:

  1. Loops through menu items
  2. Shows a dropdown if the item has children
  3. Highlights the current page
  4. 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 forloop variables in our Liquid Playground with pre-loaded product and collection data.

Key Takeaways

  1. if/elsif/else handle conditional rendering
  2. unless is cleaner than if not for negative conditions
  3. case/when is best for matching one value against many options
  4. for loops iterate over arrays with access to forloop properties
  5. limit and offset control loop boundaries
  6. break and continue give fine-grained loop control
  7. blank is safer than empty string checks for nil handling
  8. 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...