If you can read HTML, you can write Fluid. The language only adds two new pieces of syntax to plain HTML, and the rest is just rules about how those two pieces behave.
1. The basics
A Fluid template is plain HTML (or text) with two extra pieces of syntax mixed in:
Construct | Purpose | Example |
| Display a value from your site (a product name, a customer's email, etc.) |
|
| Do something — show content only if a condition is true, repeat content for every item in a list, store a value to reuse later |
|
Anything outside those two markers is left exactly as you wrote it.
Show a value
<h1>{{ product.title }}</h1>
<p>{{ product.description }}</p>
Get at nested values
Values are often grouped — a customer has an address, an address has a city. Walk into them with dots:
{{ user.address.city }}
{{ data['first-name'] }}
{{ items[0] }}Use square brackets [...] only when the name contains characters that dots can't handle (like a hyphen), or to pick a specific position out of a list.
Transform a value with filters
A filter changes how a value is displayed — uppercase it, format it as money, shorten it, and so on. Add a filter with the pipe character |. You can chain as many as you want, left to right:
{{ 'hello world' | upcase }} → HELLO WORLD
{{ product.price | money }} → $19.99
{{ article.body | strip_html | truncate:120 }}Some filters take extra information. Put it after a colon, and separate multiple values with commas:
{{ 'one two three' | replace:'two','TWO' }} → one TWO threeThe full list of filters is in Section 7.
2. Logic
if / elsif / else
{% if customer %}
Welcome back, {{ customer.first_name }}.
{% elsif visitor.first_visit %}
Welcome!
{% else %}
Hello again.
{% endif %}Available operators: ==, !=, <, >, <=, >=, combined with and / or.
{% if product.available and product.price < 50 %}
On sale!
{% endif %}elseif is also accepted as a spelling of elsif.
unless
The inverse of if. Renders the block when the condition is false.
{% unless customer.tax_exempt %}
Tax: {{ order.tax | money }}
{% endunless %}
case / when
A cleaner alternative to a long if/elsif chain when you're comparing a single value:
{% case order.status %}
{% when 'paid' %}
Thanks — your order is on the way.
{% when 'refunded' %}
Your refund has been processed.
{% when 'cancelled' %}
This order was cancelled.
{% else %}
Order status: {{ order.status }}
{% endcase %}
Truthy / falsy
These values count as false: missing variables, null, the literal false, and empty strings. Everything else is true — including the number 0. If you need to test for a non-zero number, compare it explicitly with >:
{% if cart.item_count > 0 %}...{% endif %} <!-- good -->
{% if cart.item_count %}...{% endif %} <!-- also true when count is 0 — watch out -->3. Loops
for
<ul>
{% for product in collection.products %}
<li>{{ product.title }} — {{ product.price | money }}</li>
{% endfor %}
</ul>
Loop options
{% for item in items limit:5 %}...{% endfor %}
{% for item in items offset:10 %}...{% endfor %}
{% for item in items reversed %}...{% endfor %}
Number ranges
{% for i in (1..5) %}
Page {{ i }}
{% endfor %}
Inside the loop
The forloop object gives you positional info:
Property | Meaning |
| 1-based index |
| 0-based index |
|
|
|
|
| Total iterations |
{% for item in items %}
{% if forloop.first %}<ul>{% endif %}
<li>{{ forloop.index }}. {{ item.title }}</li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}
tablerow
Renders an HTML table row per item, with configurable columns:
<table>
{% tablerow product in collection.products cols:3 %}
{{ product.title }}
{% endtablerow %}
</table>
cycle
Rotate through a list of values across iterations. Useful for striped rows:
{% for row in rows %}
<tr class="{% cycle 'odd', 'even' %}">...</tr>
{% endfor %}You can run independent cycles in parallel by naming them:
{% cycle 'colours': 'red', 'blue' %}
{% cycle 'sizes': 'small', 'large' %}
paginate
Wrap a loop to enable pagination. Inside the block, a paginate object becomes available:
{% paginate collection.products by 12 %}
{% for product in collection.products %}
<h3>{{ product.title }}</h3>
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}The paginate object has current_page, pages, next, previous, and parts for building your own pager. Or just pipe it through default_pagination / foundation_pagination.
4. Variables
assign
Create or overwrite a variable. Filters can be chained on the right-hand side:
{% assign greeting = 'hello' %}
{% assign loud_greeting = greeting | upcase %}
{% assign full_price = product.price | plus: product.shipping %}
capture
Build up a block of content and store the result in a variable. Anything between {% capture %} and {% endcapture %} is rendered and saved instead of being shown:
{% capture email_subject %}
Order {{ order.number }} — {{ shop.name }}
{% endcapture %}
<title>{{ email_subject | strip_newlines | trim }}</title>
increment / decrement
Auto-incrementing (or decrementing) counters. Each time you write {% increment widget_id %} on a page, the number goes up by one. Handy for generating unique IDs for things like accordion sections or modal dialogs:
{% increment widget_id %} → 0
{% increment widget_id %} → 1
{% increment widget_id %} → 25. Snippets (includes)
Themes are usually broken up into reusable pieces — a product card, a header bar, a footer, a search box. These are called snippets, and you drop them into a template with {% include %}. Refer to a snippet by its name, in quotes.
{% include 'product-card' %}
Passing a value to the snippet
with makes a value available inside the snippet, named after the snippet itself:
{% include 'product-card' with featured_product %}
<!-- inside the snippet, {{ product-card }} now refers to featured_product -->
Repeating a snippet for every item in a list
for runs the snippet once per item in a collection:
{% include 'product-card' for collection.products %}
Passing extra named values
Any key:value pairs become variables inside the snippet:
{% include 'product-card' product:featured_product, layout:'wide' %}6. Comments and raw output
comment
The body is stripped from the output:
{% comment %}
This is internal — never rendered.
{% endcomment %}
raw
Inside a raw block, {{ }} and {% %} are not parsed — they're emitted literally. Handy when you're outputting templates for another system:
<script type="text/x-handlebars">
{% raw %}
<p>Hello, {{ name }}!</p>
{% endraw %}
</script>
7. Filters
You've already seen the pipe syntax: {{ value | filter }}, {{ value | filter:arg }}, {{ value | first | second:'x' }}.
String
Filter | What it does |
| Uppercase the input |
| Lowercase the input |
| First letter of each word uppercased |
| Convert |
| Slugify (e.g. |
| HTML-escape |
| Remove all HTML tags |
| Remove |
| Convert newlines to |
| Remove leading/trailing whitespace |
| Append a string |
| Prepend a string |
| Replace every occurrence |
| Replace only the first occurrence |
| Remove every occurrence |
| Remove only the first occurrence |
| Truncate to |
| Truncate to |
| First / last |
| Split a string on the given separator into a list |
| URL escaping |
| Highlight a search term and crop the surrounding text |
| First N characters of an HTML string, keeping tags closed |
| Minify a string |
Numeric
Filter | Example |
|
|
|
|
|
|
|
|
|
|
| Round to nearest integer |
| A random integer up to the input |
Lists
Filter | What it does |
| Length of a list or a string. Also available as |
| First item in the list |
| Last item in the list |
| Join the items into a single string with the given separator |
| Merge two lists into one |
Date / time
{{ article.published_at | date:'%b %d, %Y' }} → Jan 14, 2026
{{ order.created_at | datetime_format }}
{{ event.starts_at | time_format }}Filter | What it does |
| Format a date using a custom mask (see specifiers below) |
| Auto-formatted date in your store's locale |
| Auto-formatted time in your store's locale |
| Auto-formatted date + time in your store's locale |
Building a date mask
A mask is a quoted string where each %X placeholder is replaced with a part of the date. Everything else (spaces, commas, slashes, words) is kept exactly as written.
{{ article.published_at | date:'%B %d, %Y' }} → January 14, 2026
{{ article.published_at | date:'%A, %b %d' }} → Wednesday, Jan 14
{{ event.starts_at | date:'%I:%M %p' }} → 09:30 AM
{{ event.starts_at | date:'%Y-%m-%d %H:%M' }} → 2026-01-14 09:30
{{ 'now' | date:'%c' }} → Wed Jan 14 09:30:00 2026You can pass the string 'now' instead of a date variable to get the current time.
Specifiers
Day
Code | Meaning | Example |
| Abbreviated weekday name |
|
| Full weekday name |
|
| Day of the month, zero-padded |
|
| Day of the year, zero-padded |
|
| Day of the week as a number (Sunday = 0) |
|
Month
Code | Meaning | Example |
| Abbreviated month name |
|
| Full month name |
|
| Month as a number, zero-padded |
|
Year
Code | Meaning | Example |
| Two-digit year |
|
| Four-digit year |
|
| Week of the year, starting on Sunday |
|
| Week of the year, starting on Monday |
|
Time
Code | Meaning | Example |
| Hour, 24-hour clock, zero-padded |
|
| Hour, 12-hour clock, zero-padded |
|
| Minute, zero-padded |
|
| Second, zero-padded |
|
| AM/PM indicator |
|
| Time zone name |
|
Shortcuts and literals
Code | Meaning |
| Default date + time ( |
| Default date only ( |
| Default time only ( |
| A literal |
Money
These use your store's currency setting (General Settings → Preferences):
Filter | Output |
|
|
|
|
|
|
| Same, formatted for international locales |
|
|
URLs and links
These automatically respect your site's domain, CDN settings, and theme directory:
Filter | What it does |
| URL for a theme asset (image, JS, CSS) |
| URL for a global (cross-site) asset |
| URL for a product/collection image |
| Convenience URL filters |
| Permalink builder for a product |
|
|
| Link to a content page |
| Tag link helpers |
| URL for built-in site actions (e.g. |
HTML helpers
{{ 'styles.css' | asset_url | stylesheet_tag }}
{{ 'app.js' | asset_url | script_tag }}
{{ 'logo.png' | asset_url | img_tag:'My logo' }}Filter | Output |
|
|
|
|
|
|
Forms
Filter | What it does |
| Current value for a form field |
| Display a default-field setting |
| Render a form option |
| Render the error message for a field |
| A |
| A |
| Pick the right word based on count |
| Translate preset size names |
Pagination
Filter | What it does |
| Returns the current page size |
| A ready-made pager block |
| Pager styled for the Foundation framework |
Misc
Filter | What it does |
| Convert any value to a JSON string. Handy for passing data into inline JavaScript |
| Length of a string or list, or count of items in a record |
8. Common patterns
Defaulting a value
{% if product.subtitle %}
<p>{{ product.subtitle }}</p>
{% else %}
<p>No description available.</p>
{% endif %}
Conditional CSS class
<li class="product {% if product.available %}in-stock{% else %}sold-out{% endif %}">
...
</li>
Reading a query string flag
{% if query.preview == 'true' %}
<div class="preview-banner">Preview mode</div>
{% endif %}
Loop with a separator (no trailing comma)
{% for tag in product.tags %}
{{ tag }}{% unless forloop.last %}, {% endunless %}
{% endfor %}
Pass data to inline JavaScript
<script> var product = {{ product | json }}; </script>
Skip empty collections cleanly
{% if collection.products.size > 0 %}
{% for product in collection.products %}
...
{% endfor %}
{% else %}
<p>No products in this collection yet.</p>
{% endif %}
Paginated grid
{% paginate collection.products by 24 %}
<div class="grid">
{% for product in collection.products %}
{% include 'product-card' with product %}
{% endfor %}
</div>
{{ paginate | default_pagination }}
{% endpaginate %}9. Theme-specific tags
In addition to the generic Fluid tags above, your theme has special tags for structural pieces of the page. These let you build the skeleton of a layout without having to know how the underlying page works:
Tag | Use |
| Switch to a named layout |
| Mark the |
| Mark the |
| Drop-in for additional |
| Outputs the current page's main content |
| Insert a named section |
| Render the site navigation |
| Render the sitemap |
| Open a managed form (CSRF, validation, submission) |
| Render a slideshow widget |
| Editable content placeholder |
| Inline editing markers (admin only) |
| Read template-level settings |
| Theme-flavoured iteration helper |
| Render the items-per-page selector |
| Render the collection sort dropdown |
| Render a content page |
| Output theme-managed content |
| Output a managed attribute |
| Define / output a named block |
| Render review widgets |
| Include a globally-managed snippet |
Social embeds
Tag | What it embeds |
| Facebook share / like buttons |
| Twitter / X share button |
| LinkedIn share button |
| YouTube video embed |
| Google+ share button (legacy) |
| AddThis share bar |
If you need the exact attribute list for any of these, check the example snippets in your theme — most themes ship with a working example of each.
10. Quick reference
<!-- output -->
{{ value }}
{{ value | filter }}
{{ value | filter:arg }}
{{ value | filter:arg1,arg2 | filter2 }}
<!-- logic -->
{% if x == y %}...{% elsif x == z %}...{% else %}...{% endif %}
{% unless x %}...{% endunless %}
{% case x %}{% when 'a' %}...{% when 'b' %}...{% else %}...{% endcase %}
<!-- loops -->
{% for item in items %}...{% endfor %}
{% for item in items limit:5 offset:10 reversed %}...{% endfor %}
{% for i in (1..10) %}...{% endfor %}
{% tablerow item in items cols:3 %}...{% endtablerow %}
{% cycle 'a', 'b', 'c' %}
{% paginate items by 20 %}...{% endpaginate %}
<!-- variables -->
{% assign x = 'hello' %}
{% capture x %}...{% endcapture %}
{% increment counter %}
{% decrement counter %}
<!-- includes -->
{% include 'partial' %}
{% include 'partial' with value %}
{% include 'partial' for collection %}
{% include 'partial' key:value %}
<!-- everything else -->
{% comment %}...{% endcomment %}
{% raw %}...{% endraw %}
11. Troubleshooting
Symptom | Likely cause |
| The variable doesn't exist in this scope, or it's empty/false |
Filter has no effect | Filter name typo, or the input type doesn't match (e.g. |
| Wrong syntax — must be |
| Right-hand side is empty or undefined |
| The snippet name has characters other than letters, digits, |
Loop output looks doubled | A nested |
|
|
If a page renders blank or shows an error message instead of your content, the template hit an error. Check your most recent edit and confirm:
every
{% if %}has a matching{% endif %}(same forfor,unless,case,capture,paginate,tablerow,comment,raw),filter arguments are quoted when they should be (
replace:'a','b', notreplace:a,b— unlessaandbare variables you defined),snippet names are in quotes inside
include({% include 'foo' %}, not{% include foo %}).
