Tags provide the control structures that make Liquid a true templating language rather than just a substitution engine. While keys access data, operators compare it, and filters transform it, tags control the flow and logic of your templates. They enable conditional rendering, loops, variable assignment, and other programmatic features essential for creating sophisticated notifications.
What are Liquid tags?
Tags are wrapped in curly braces with percent signs {% %} and provide programming constructs like conditionals, loops, and variable assignment. Unlike keys (which output data) or filters (which transform data), tags control what gets rendered and how.
There are three main categories of tags:
- Control flow tags determine what content appears based on conditions.
- Iteration tags loop through collections to generate repeated content.
- Variable tags create and manipulate values within your template.
Let's explore each category in depth.
Control flow tags
Control flow tags let you show or hide content based on conditions, creating dynamic templates that adapt to your data.
The if tag
The if tag is your primary tool for conditional rendering. Use it when you want to display content only when certain conditions are true:
{
"user": {
"subscription_status": "active",
"subscription_end_date": "2024-12-31",
"plan": "premium"
}
}{% if user.subscription_status == "active" %}
✓ Your subscription is active until {{ user.subscription_end_date | date: "%B %d, %Y" }}.
{% endif %}
{% if user.plan == "premium" %}
You have access to all premium features!
{% endif %}✓ Your subscription is active until December 31, 2024.
You have access to all premium features!The elsif and else tags
Use elsif and else to handle multiple conditions. Liquid evaluates each condition from top to bottom, executing the first block that matches and skipping the rest. You can have as many elsif branches as you need, and the final else acts as a catch-all for anything that didn't match the previous conditions.
{
"user": {
"subscription_status": "trialing",
"trial_days_remaining": 5
}
}{% if user.subscription_status == "active" %}
Your subscription is active. Thank you for being a subscriber!
{% elsif user.subscription_status == "trialing" %}
Your trial ends in {{ user.trial_days_remaining }} days. Upgrade to continue!
{% elsif user.subscription_status == "expired" %}
Your subscription has expired. Renew to regain access.
{% else %}
Start your free trial today!
{% endif %}Your trial ends in 5 days. Upgrade to continue!The unless tag
The unless tag is the inverse of if—it executes when a condition is false:
{
"user": {
"email_verified": false,
"profile_complete": false
}
}{% unless user.email_verified %}
⚠️ Please verify your email address to unlock all features.
{% endunless %}
{% unless user.profile_complete %}
Complete your profile to get personalized recommendations.
{% endunless %}⚠️ Please verify your email address to unlock all features.
Complete your profile to get personalized recommendations.The case and when tags
When you have multiple conditions based on a single value, case provides cleaner syntax than multiple if statements. Think of this like a multiple-choice question where you're checking "which one of these options matches?" Instead of asking "is it A? is it B? is it C?" repeatedly, you ask once and then list all the possible answers. Each when branch represents one possible value, and like if/elsif, Liquid executes only the first matching branch.
{
"notification": {
"type": "payment_failed",
"amount": 99.99
}
}{% case notification.type %}
{% when "payment_success" %}
✓ Your payment of ${{ notification.amount }} was successful.
{% when "payment_failed" %}
✗ We couldn't process your payment of ${{ notification.amount }}. Please update your card.
{% when "subscription_renewed" %}
✓ Your subscription has been renewed for ${{ notification.amount }}.
{% when "refund_processed" %}
A refund of ${{ notification.amount }} has been issued to your account.
{% else %}
You have a new notification.
{% endcase %}✗ We couldn't process your payment of $99.99. Please update your card.Iteration tags
Iteration tags let you loop through arrays and generate repeated content dynamically. Think of a loop like giving the same instruction multiple times, or "do this for each item in the list." Instead of manually writing the same code over and over for each item, you write it once and Liquid repeats it automatically for every element in your collection.
The for loop
The for tag iterates through arrays, executing the template content for each element. You're essentially saying "for each item in this list, do the following..." and Liquid takes care of going through each one.
When you write {% for item in order.items %}, you're creating a temporary name (item) that represents whichever element you're currently looking at in the loop. You can name this anything you want. It's just a label you use to access the current element. Inside the loop, you can use this name with dot notation (like item.name or item.price) to access the properties of the current element.
{
"order": {
"items": [
{
"name": "T-Shirt",
"price": 25,
"quantity": 2
},
{
"name": "Jeans",
"price": 80,
"quantity": 1
},
{
"name": "Sneakers",
"price": 120,
"quantity": 1
}
]
}
}Order Summary:
{% for item in order.items %}
- {{ item.name }}: ${{ item.price }} × {{ item.quantity }} = ${{ item.price | times: item.quantity }}
{% endfor %}Order Summary:
- T-Shirt: $25.00 × 2 = $50.0
- Jeans: $80.00 × 1 = $80.0
- Sneakers: $120.00 × 1 = $120.0Loop modifiers: limit and offset
When using a for loop on an array, you can use loop modifiers to control how many iterations run and where to start.
limit: restricts the number of iterations.offset: skips a number of items before the loop begins.
{
"activities": [
{
"action": "Logged in",
"time": "2024-11-01 09:00"
},
{
"action": "Updated profile",
"time": "2024-11-01 09:15"
},
{
"action": "Made purchase",
"time": "2024-11-01 10:30"
},
{
"action": "Logged out",
"time": "2024-11-01 11:00"
},
{
"action": "Logged in",
"time": "2024-11-02 08:00"
}
]
}Recent Activities:
{% for activity in activities limit: 3 %}
{{ activity.action }} at {{ activity.time }}
{% endfor %}
Older Activities (skipping first 3):
{% for activity in activities offset: 3 %}
{{ activity.action }} at {{ activity.time }}
{% endfor %}Recent Activities (top 3):
Logged in at 2024-11-01 09:00
Updated profile at 2024-11-01 09:15
Made purchase at 2024-11-01 10:30
Older Activities (skipping first 3):
Logged out at 2024-11-01 11:00
Logged in at 2024-11-02 08:00The forloop object
Inside a for loop, Liquid provides the forloop object with helpful metadata about the current iteration:
{
"team_members": [
{
"name": "Sarah",
"role": "Engineer"
},
{
"name": "Alex",
"role": "Designer"
},
{
"name": "Jordan",
"role": "Product Manager"
}
]
}{% for member in team_members %}
{% if forloop.first %}
=== Team Directory ===
{% endif %}
{{ forloop.index }}. {{ member.name }} - {{ member.role }}
{% if forloop.last %}
=== Total: {{ forloop.length }} members ===
{% endif %}
{% endfor %}=== Team Directory ===
1. Sarah - Engineer
2. Alex - Designer
3. Jordan - Product Manager
=== Total: 3 members ===Available forloop properties:
forloop.index- Current iteration number (1-based)forloop.index0- Current iteration number (0-based)forloop.first-trueif this is the first iterationforloop.last-trueif this is the last iterationforloop.length- Total number of iterationsforloop.rindex- Number of iterations remaining (counting down, 1-based)forloop.rindex0- Number of iterations remaining (counting down, 0-based)
Handling empty arrays with else
You can provide fallback content when a loop has no items to iterate:
{
"notifications": []
}{% for notification in notifications %}
- {{ notification.message }}
{% else %}
No new notifications to display.
{% endfor %}No new notifications to display.Loop examples with conditionals
Combine loops with conditional logic for sophisticated rendering:
{
"notifications": [
{
"message": "New comment on your post",
"unread": true,
"priority": "low"
},
{
"message": "Payment received",
"unread": true,
"priority": "high"
},
{
"message": "Profile updated",
"unread": false,
"priority": "low"
}
]
}{% for notification in notifications %}
{% if forloop.first %}📬 Notifications{% endif %}
{% if notification.priority == "high" %}🔴{% elsif notification.unread %}🔵{% else %}⚪{% endif %}
{{ forloop.index }}. {{ notification.message }}
{% if notification.unread %}[NEW]{% endif %}
{% if forloop.last %}
─────────────
Total: {{ forloop.length }} notifications
{% endif %}
{% endfor %}📬 Notifications
🔵
1. New comment on your post
[NEW]
🔴 2. Payment received
[NEW]
⚪ 3. Profile updated
─────────────
Total: 3 notificationsVariable tags
Variable tags let you create and manipulate values within your template, making complex templates more readable and maintainable.
The assign tag
Use assign to create variables that store values or computed results. Think of a variable like a labeled box where you can store a value to use later. Instead of writing a long calculation or accessing the same data multiple times, you calculate or grab it once, give it a name, and then reuse that name whenever you need it. This makes your templates easier to read and helps avoid repeating the same logic.
{
"product": {
"price": 99.99,
"discount_rate": 0.2,
"tax_rate": 0.08
}
}{% assign discount_amount = product.price | times: product.discount_rate %}
{% assign discounted_price = product.price | minus: discount_amount %}
{% assign tax_amount = discounted_price | times: product.tax_rate %}
{% assign final_price = discounted_price | plus: tax_amount %}
Original Price: ${{ product.price }}
Discount (20%): -${{ discount_amount | round: 2 }}
Subtotal: ${{ discounted_price | round: 2 }}
Tax: ${{ tax_amount | round: 2 }}
Final Price: ${{ final_price | round: 2 }}Original Price: $99.99
Discount (20%): -$20.00
Subtotal: $79.99
Tax: $6.40
Final Price: $86.39The capture tag
The capture tag stores a block of content (which may include template logic) in a variable. While assign works with simple values or single calculations, capture is like putting together a whole paragraph or section, including text, keys, filters, and even conditional logic, and saving all of that as one complete piece that you can output later. It's particularly useful when you need to build complex text once and use it multiple times, or when you want to generate content conditionally before deciding where to display it.
{
"user": {
"first_name": "Sarah",
"last_login_days_ago": 45
}
}{% capture greeting %}
{% if user.last_login_days_ago > 30 %}
Welcome back, {{ user.first_name }}! It's been a while—we've missed you!
{% elsif user.last_login_days_ago > 7 %}
Hey {{ user.first_name }}, good to see you again!
{% else %}
Hi {{ user.first_name }}!
{% endif %}
{% endcapture %}
{{ greeting | strip }}Welcome back, Sarah! It's been a while—we've missed you!Variables with arrays
You can use variables to simplify working with repeated array access:
{
"cart": {
"items": [
{
"name": "Widget A",
"price": 29.99
},
{
"name": "Widget B",
"price": 49.99
},
{
"name": "Widget C",
"price": 19.99
}
]
}
}{% assign total = 0 %}
{% for item in cart.items %}
{% assign item_price = item.price %}
{% assign total = total | plus: item_price %}
{% endfor %}
Cart Summary:
{% for item in cart.items %}
- {{ item.name }}: ${{ item.price }}
{% endfor %}
─────────────
Total: ${{ total }}Cart Summary:
- Widget A: $29.99
- Widget B: $49.99
- Widget C: $19.99
─────────────
Total: $99.97Comment tags
Use comments to document your templates without affecting output:
{# This is a single-line comment #}
{% comment %}
This is a multi-line comment.
It can span multiple lines and won't appear in the output.
Useful for explaining complex logic or temporarily disabling code.
{% endcomment %}
Hello {{ user.name }}!Practical tag combinations
Real-world notification templates often combine multiple tag types. Here's a comprehensive example:
{
"user": {
"name": "Alex",
"plan": "pro",
"usage_percentage": 85
},
"features": [
{
"name": "API Access",
"enabled": true,
"usage": 8500
},
{
"name": "Advanced Analytics",
"enabled": true,
"usage": 230
},
{
"name": "Priority Support",
"enabled": false,
"usage": 0
}
]
}Hi {{ user.name }},
{% if user.usage_percentage >= 80 %}
⚠️ You've used {{ user.usage_percentage }}% of your {{ user.plan | upcase }} plan this month.
{% if user.usage_percentage >= 100 %}
You've exceeded your limit. Consider upgrading to avoid overages.
{% elsif user.usage_percentage >= 90 %}
You're approaching your limit. Upgrade now to avoid disruption.
{% else %}
You're on track to hit your limit before month-end.
{% endif %}
{% endif %}
Feature Usage:
{% for feature in features %}
{% if feature.enabled %}
✓ {{ feature.name }}: {{ feature.usage | format_number }} uses
{% else %}
✗ {{ feature.name }}: Not enabled on your plan
{% endif %}
{% endfor %}
{% assign enabled_count = 0 %}
{% for feature in features %}
{% if feature.enabled %}
{% assign enabled_count = enabled_count | plus: 1 %}
{% endif %}
{% endfor %}
You're using {{ enabled_count }} of {{ features.size }} available features.Hi Alex,
⚠️ You've used 85% of your PRO plan this month.
You're on track to hit your limit before month-end.
Feature Usage:
✓ API Access: 8,500 uses
✓ Advanced Analytics: 230 uses
✗ Priority Support: Not enabled on your plan
You're using 2 of 3 available features.Best practices for using tags
1. Keep conditional logic readable
Break complex conditions into multiple lines or use intermediate variables:
<!-- Less readable -->
{% if user.plan == "premium" and user.verified and user.payment_method or user.trial_days > 0 %}
<!-- More readable -->
{% assign is_premium_verified = user.plan == "premium" and user.verified and user.payment_method %}
{% assign has_trial = user.trial_days > 0 %}
{% if is_premium_verified or has_trial %}Use case for multiple value checks
When checking a single variable against multiple values, case is cleaner than multiple if statements.
Provide else clauses in loops
Always handle the empty array case with an else clause in your for loops to prevent blank output.
Use variables to avoid repetition
If you're computing the same value multiple times, assign it to a variable once and reuse it.
Comment complex logic
Use comment tags to explain non-obvious conditional logic or calculations.
Tags are what transform Liquid from a simple substitution language into a full-featured templating system. They give you the power to create notifications that adapt intelligently to your data, handle edge cases gracefully, and maintain clean, readable template code.