Free guide · every tag and filter · about 60 minutes

Liquid explained simply

Every part of Liquid, one by one: what it does, when and why you use it, and a small Shopify example with the real output. No jargon.

What is Liquid?

Liquid is the language Shopify uses to put store data into a page.

Think of a wedding card printer. They have one card design with blank spaces: "Dear ______, you are invited…". For each guest, they fill in the name. The design stays the same. Only the blanks change.

A Shopify theme is that card design. Liquid fills in the blanks: the product name, the price, the photo, the cart total.

How it works

  1. A shopper opens a page, like a product page.
  2. Shopify reads the theme file and finds the Liquid code.
  3. Shopify replaces the Liquid with real data (this product's title, this product's price).
  4. The shopper's browser gets plain HTML. They never see the Liquid.

How to try every example

  1. Open Shopify admin → Online Store → Themes → Horizon → Customize.
  2. Click Add section (+) and pick Custom Liquid.
  3. Paste any example from this page into the box.
  4. Look at the preview on the right.
Check: You paste {{ shop.name }} into a Custom Liquid section and the preview shows your store's name.

The three building blocks

Every bit of Liquid is one of these three things.

1. Output: double curly braces

Double curly braces print something on the page. Think of them as "show this here".

Input
{{ product.title }}
Output
Masala Chai

2. Tags: curly brace and percent

Tags are the logic: "if this, do that", "repeat this for every product". They do not print anything by themselves.

Input
{% if product.available %}
  In stock
{% endif %}
Output
In stock

3. Filters: the pipe sign |

A filter changes a value before it is printed. Think of a photo filter: same photo, new look. You add it with the pipe sign |.

Input
{{ product.title | upcase }}
Output
MASALA CHAI

You can chain filters. They run from left to right:

Input
{{ product.title | upcase | append: " - SALE" }}
Output
MASALA CHAI - SALE

Basics

Types: the kinds of values

Every value in Liquid is one of these types.

TypeWhat it isExample
StringText, inside quotes"Hello" or 'Hello'
NumberWhole or decimal numbers25 or -39.75
BooleanYes or notrue or false
Nil"Nothing here"a field with no value
ArrayA list of thingsproduct.tags, collection.products
EmptyDropAn object that does not exista page handle that is wrong
Input
{% assign name = "Chai" %}
{% assign price = 250 %}
{% assign on_sale = true %}
{{ name }} costs {{ price }}. On sale: {{ on_sale }}
Output
Chai costs 250. On sale: true

Arrays (lists). You read one item by its position in square brackets. Counting starts at 0. A negative number counts from the end.

Input
{{ product.tags[0] }}
{{ product.tags[-1] }}
Output
organic
bestseller

You cannot type a list directly in Liquid. To make one, use the split filter (see Filters below).

Nil prints nothing. If a value does not exist, Liquid prints an empty space, not an error.

Input
Hello {{ customer.first_name }}!
Output (shopper is not logged in)
Hello !

EmptyDrop and empty. To check if something exists, or if a list or text is empty, compare it with empty:

Input
{% if product.tags == empty %}
  This product has no tags.
{% endif %}

Operators: comparing values

Operators let you ask questions inside if and unless.

OperatorMeaningExample
==is equal toproduct.type == "Tea"
!=is not equal toproduct.type != "Tea"
>greater thanproduct.price > 50000
<less thancart.item_count < 3
>=greater than or equalproduct.price >= 100000
<=less than or equalvariant.inventory_quantity <= 5
andboth must be truea and b
orat least one must be truea or b
containstext or list has thisproduct.tags contains "sale"

contains works two ways:

Input
{% if product.title contains "Chai" %}
  This is a chai product.
{% endif %}

{% if product.tags contains "sale" %}
  On sale!
{% endif %}
Input
{% if true or false and false %}
  This prints, because "false and false" is checked first.
{% endif %}

Truthy and falsy: what counts as "yes"

When you write {% if something %}, Liquid must decide: yes or no?

Only two things are "no" (falsy): false and nil. Everything else is "yes" (truthy), even an empty text "" and the number 0.

ValueCounts as
trueyes
falseno
nil (nothing)no
any text, even ""yes
0 and any numberyes
any list, even emptyyes

This surprises everyone once. An empty text is still "yes":

Input (the metafield exists but is empty)
{% if product.metafields.custom.subtitle %}
  <h2>{{ product.metafields.custom.subtitle }}</h2>
{% endif %}
Output
<h2></h2>

Whitespace control: removing empty lines

Every Liquid tag leaves an empty line in the HTML, even if it prints nothing. Add a dash - inside the braces to remove the space on that side: {%- and -%}, or {{- and -}}.

Use it when: the HTML looks messy, or extra spaces break your layout (for example spaces inside a price or a class name).

Input (without dashes)
{% assign tea = "Masala Chai" %}
{{ tea }}
Output (note the empty first line)

Masala Chai
Input (with a dash)
{% assign tea = "Masala Chai" -%}
{{ tea }}
Output
Masala Chai

Horizon uses {%- and -%} almost everywhere. Now you know why.

Variations: core Liquid vs Shopify Liquid

Liquid is used in many places. Each place adds its own extras.

  • Core Liquid (this page): the tags, filters and basics that work everywhere.
  • Shopify Liquid: core Liquid plus store objects (product, cart, collection, customer) and store filters (money, image_url, asset_url, t). Full list: shopify.dev/docs/api/liquid (opens in a new tab).
  • Jekyll Liquid: used for blogs on GitHub Pages, with its own extras.

Why it matters: if a filter from a Jekyll tutorial does not work in your theme, it is probably Jekyll-only.

Tags: control flow (making decisions)

These tags decide which part of the code runs.

if

Runs the code inside only if the condition is true.

Use it when: something should show only sometimes: a "Sold out" label, a sale badge, a message for logged-in customers.

Input
{% if product.available == false %}
  <span class="badge">Sold out</span>
{% endif %}
Output (product is sold out)
<span class="badge">Sold out</span>

unless

The opposite of if: runs the code only if the condition is false. "Unless it is raining, we go out."

Use it when: "show this, except when…" reads more naturally than if … != ….

Input
{% unless product.available %}
  Sold out
{% endunless %}

This is the same as {% if product.available == false %}. Use whichever is easier to read.

elsif and else

Add more choices to an if. elsif = "otherwise, if…". else = "in every other case".

Use it when: there are 3 or more possible results, like stock levels.

Input
{% assign qty = product.selected_or_first_available_variant.inventory_quantity %}
{% if qty <= 0 %}
  Sold out
{% elsif qty < 5 %}
  Only {{ qty }} left!
{% else %}
  In stock
{% endif %}
Output (3 in stock)
Only 3 left!

case and when

Checks one value against a list of choices. Like a lift: press 1, 2 or 3, and you go to that floor.

Use it when: you compare the same thing many times (product type, number of blocks, a setting). It is cleaner than many elsif lines.

Input
{% case product.type %}
  {% when "Tea" %}
    Brew for 3 minutes.
  {% when "Coffee", "Espresso" %}
    Brew for 4 minutes.
  {% else %}
    See the label for instructions.
{% endcase %}
Output (product type is Tea)
Brew for 3 minutes.

One when can take several values, separated by commas or or.

Tags: iteration (repeating)

These tags repeat a piece of code, once for each item in a list.

for

Runs the code once for every item.

Use it when: you show many things in the same way: products in a collection, items in the cart, tags on a product.

Input
{% for product in collection.products %}
  <p>{{ product.title }}</p>
{% endfor %}
Output
<p>Masala Chai</p>
<p>Green Tea</p>
<p>Clay Cup</p>

for … else

The else part runs when the list is empty.

Use it when: you want a friendly message instead of a blank space.

Input
{% for item in cart.items %}
  {{ item.title }}
{% else %}
  Your cart is empty.
{% endfor %}
Output (empty cart)
Your cart is empty.

break

Stops the loop right away.

Use it when: you found what you were looking for and do not need to check the rest.

Input
{% for i in (1..5) %}
  {% if i == 4 %}{% break %}{% endif %}
  {{ i }}
{% endfor %}
Output
1 2 3

continue

Skips the current item and moves to the next one.

Use it when: some items should be left out, like sold-out products in a "featured" row.

Input
{% for product in collection.products %}
  {% unless product.available %}{% continue %}{% endunless %}
  {{ product.title }}
{% endfor %}
Output (Green Tea is sold out)
Masala Chai Clay Cup

limit and offset

limit = how many items at most. offset = how many to skip at the start.

Use it when: you show "the first 4 products", or "products 5 to 8".

Input
{% for product in collection.products limit: 2 %}
  {{ product.title }}
{% endfor %}

{% for product in collection.products offset: 2 %}
  {{ product.title }}
{% endfor %}
Output
Masala Chai Green Tea
Clay Cup

offset: continue starts where the last loop over the same list stopped. Useful to split one list into two rows.

range

Loops over numbers instead of a list. Write it as (start..end).

Use it when: you need to repeat something a fixed number of times: 5 rating stars, 3 empty placeholder cards.

Input
{% for i in (1..5) %}★{% endfor %}
Output
★★★★★

The end number can be a variable: (1..rating).

reversed

Loops from the last item to the first.

Use it when: you want newest first, or the opposite order of the list.

Input
{% for i in (1..3) reversed %}{{ i }} {% endfor %}
Output
3 2 1

Note the spelling: the loop word is reversed, the filter is reverse.

forloop (information about the loop)

Inside every for loop, Liquid gives you a helper called forloop. It knows where you are in the loop.

PropertyWhat it gives you
forloop.indexposition, starting at 1
forloop.index0position, starting at 0
forloop.firsttrue on the first item
forloop.lasttrue on the last item
forloop.lengthhow many items in total
forloop.rindexhow many are left, counting this one
forloop.parentloopthe outer loop, when loops are inside loops

Use it when: the first or last item needs something different, like commas between words but not after the last one.

Input
{% for tag in product.tags %}
  {{ tag }}{% unless forloop.last %}, {% endunless %}
{% endfor %}
Output
organic, assam, bestseller

cycle

Gives the next value from a list each time it is called, then starts again. Like a traffic light: red, yellow, green, red…

Use it when: you alternate styles, like striped table rows (odd, even) or left/right image positions.

Input
{% for product in collection.products %}
  <div class="{% cycle 'row-light', 'row-dark' %}">{{ product.title }}</div>
{% endfor %}
Output
<div class="row-light">Masala Chai</div>
<div class="row-dark">Green Tea</div>
<div class="row-light">Clay Cup</div>

If you use two separate cycles in one file, give each a name so they do not mix: {% cycle 'colors': 'red', 'blue' %}.

tablerow

Builds the rows and cells of an HTML table for you. You write the <table> tags, it writes the <tr> and <td>.

Use it when: you need a real table, like a size chart or a product comparison. For product grids, use for with CSS grid instead.

Input
<table>
  {% tablerow product in collection.products cols: 2 %}
    {{ product.title }}
  {% endtablerow %}
</table>
Output
<table>
  <tr class="row1"><td class="col1">Masala Chai</td><td class="col2">Green Tea</td></tr>
  <tr class="row2"><td class="col1">Clay Cup</td></tr>
</table>

tablerow also takes cols, limit, offset and ranges like (1..6). Inside, a helper called tablerowloop works like forloop, plus col, row, col_first and col_last.

Tags: template (how files work together)

comment

Text between {% comment %} and {% endcomment %} is not shown and not run.

Use it when: you leave a note for the next developer, or turn off a piece of code for a while without deleting it.

Input
{% comment %}
  Old banner. Turned off for Diwali. Bring it back in January.
{% endcomment %}
Output

Inline comment (#)

A shorter comment for one line: start with # inside a tag.

Use it when: you want a quick note on one line.

Input
{% # Show the badge only for sale items %}
{%- if product.tags contains "sale" -%}SALE{%- endif -%}
Output (sale item)
SALE

raw

Everything between {% raw %} and {% endraw %} is printed exactly as written, without running it as Liquid.

Use it when: you need to show curly braces on the page, for example JavaScript template code (like Vue or Handlebars) that also uses double curly braces, or a tutorial that shows Liquid code.

Input
{% raw %}Write {{ product.title }} to print the title.{% endraw %}
Output
Write {{ product.title }} to print the title.

liquid

Lets you write many tags inside one tag, one per line, without repeating {% and %} every time.

Use it when: you have a lot of logic and few outputs. It is much easier to read. Horizon uses it at the top of many blocks.

Input
{% liquid
  assign qty = product.selected_or_first_available_variant.inventory_quantity
  if qty < 5
    assign stock_class = 'stock--low'
  else
    assign stock_class = 'stock--ok'
  endif
%}
<p class="{{ stock_class }}">In stock</p>
Output (3 left)
<p class="stock--low">In stock</p>

echo

Prints a value, like double curly braces, but it works inside the liquid tag.

Use it when: you are inside {% liquid %} and need to print something.

Input
{% liquid
  for product in collection.products limit: 2
    echo product.title | upcase
    echo ' / '
  endfor
%}
Output
MASALA CHAI / GREEN TEA /

render

Inserts another file (a snippet) from the snippets folder.

Use it when: the same piece of HTML is used in many places, like a product card. Write it once, use it everywhere. Change it once, it changes everywhere.

Input
{% render 'product-card', product: product, show_vendor: true %}

Things to know:

  • Do not write .liquid at the end of the name.
  • The snippet cannot see your variables. You must pass them in, like product: product above. This keeps snippets safe and predictable.
  • with: pass one object under a name: {% render 'product-card' with featured as product %}
  • for: render the snippet once per item: {% render 'product-card' for collection.products as product %}. Inside, forloop works.

include (old, do not use)

The old way to insert a snippet. It could see and change the parent's variables, which made code slow and confusing. Use render instead. You will still see include in old themes, so it is good to recognise it.

Tags: variables (saving values)

assign

Saves a value under a name so you can use it later. Like writing a phone number on a sticky note.

Use it when: you use the same value many times, or a long expression is hard to read.

Input
{% assign variant = product.selected_or_first_available_variant %}
{% assign is_cheap = false %}
{% if variant.price < 50000 %}{% assign is_cheap = true %}{% endif %}
Under ₹500: {{ is_cheap }}
Output (₹250 product)
Under ₹500: true

Put text in quotes: {% assign label = "New" %}. Numbers, true and false have no quotes.

capture

Saves everything between the tags as text, including HTML and other outputs.

Use it when: you build a longer piece of text or HTML from several parts, then use it later (or more than once).

Input
{% capture share_text %}I just bought {{ product.title }} from {{ shop.name }}!{% endcapture %}
<a href="https://wa.me/?text={{ share_text | url_encode }}">Share on WhatsApp</a>
Output
<a href="https://wa.me/?text=I+just+bought+Masala+Chai+from+Chai+Corner%21">Share on WhatsApp</a>

What you capture is always text, even if it looks like a number.

increment

Makes a counter that starts at 0, prints it, and adds 1 each time.

Use it when: you need unique numbers on the page, like IDs for many accordions: faq-0, faq-1, faq-2.

Input
{% increment counter %}
{% increment counter %}
{% increment counter %}
Output
0
1
2

The counter is separate from assign. If you assign counter = 10 and then increment counter, you still get 0, 1, 2, and counter stays 10.

decrement

Like increment, but starts at -1 and goes down by 1 each time.

Input
{% decrement countdown %}
{% decrement countdown %}
{% decrement countdown %}
Output
-1
-2
-3

Filters: text

These filters change text (strings).

append

Adds text to the end.

Use it when: you build a link, a file name or a CSS class from parts.

Input
{{ product.url | append: "?ref=homepage" }}
Output
/products/masala-chai?ref=homepage

prepend

Adds text to the start.

Use it when: you add a label in front, like "Brand: " or a currency word.

Input
{{ product.vendor | prepend: "Brand: " }}
Output
Brand: Chai Corner

upcase

Makes ALL LETTERS CAPITAL.

Use it when: you need shouting text, like a SALE badge.

Input
{{ "sale" | upcase }}
Output
SALE

downcase

Makes all letters small.

Use it when: you compare text and do not want capitals to matter, or you build a CSS class from a name.

Input
{{ "Green Tea" | downcase }}
Output
green tea

capitalize

Makes the first letter capital and the rest small.

Use it when: text comes in messy (all caps or all small) and you want it to look tidy.

Input
{{ "mASALA chai" | capitalize }}
Output
Masala chai

strip, lstrip, rstrip

Remove spaces and new lines from the edges. strip = both sides, lstrip = left only, rstrip = right only. Spaces between words stay.

Use it when: a setting or metafield has extra spaces that break your layout or a comparison.

Input
[{{ "   Masala Chai   " | strip }}]
[{{ "   Masala Chai   " | lstrip }}]
[{{ "   Masala Chai   " | rstrip }}]
Output
[Masala Chai]
[Masala Chai   ]
[   Masala Chai]

squish

Removes spaces at both ends and turns any group of spaces or new lines inside into one space.

Use it when: you build a class list or style text with capture across many lines, and want it on one clean line.

Input
{{ "  card     card--sale
   card--small  " | squish }}
Output
card card--sale card--small

strip_html

Removes all HTML tags, leaving only the text.

Use it when: you need plain text from a description, for example for a meta description or a short preview.

Input
{{ "<p>Strong <strong>Assam</strong> tea.</p>" | strip_html }}
Output
Strong Assam tea.

strip_newlines

Removes all line breaks.

Use it when: text must be on one line, like inside a JavaScript string or a data attribute.

Input
{% capture note %}
Hello
there
{% endcapture %}
{{ note | strip_newlines }}
Output
Hellothere

newline_to_br

Adds an HTML line break <br /> at every new line.

Use it when: the merchant types text with Enter in a plain text box (like an address or a note), and you want those lines to show on the page.

Input
{{ "Shop 12
MG Road
Pune" | newline_to_br }}
Output
Shop 12<br />
MG Road<br />
Pune

remove, remove_first, remove_last

Delete a piece of text. remove = every time it appears. remove_first = only the first time. remove_last = only the last time.

Use it when: you clean up text, like removing a brand name from titles, or a word like "Default".

Input
{{ "Chai Corner Masala Chai" | remove: "Chai Corner " }}
{{ "tea, tea, tea" | remove_first: "tea, " }}
{{ "tea, tea, tea" | remove_last: ", tea" }}
Output
Masala Chai
tea, tea
tea, tea

replace, replace_first, replace_last

Swap one piece of text for another. Same "every / first / last" idea as remove.

Use it when: you change words or characters, like turning a handle into a readable name.

Input
{{ "masala-chai-500g" | replace: "-", " " }}
{{ "a-b-c" | replace_first: "-", "+" }}
{{ "a-b-c" | replace_last: "-", "+" }}
Output
masala chai 500g
a+b-c
a-b+c

truncate

Cuts text to a number of characters, and adds ... at the end. The ... counts in the number.

Use it when: long titles or descriptions break your card design.

Input
{{ "Organic Assam Masala Chai with Cardamom" | truncate: 20 }}
{{ "Organic Assam Masala Chai with Cardamom" | truncate: 20, " →" }}
Output
Organic Assam Mas...
Organic Assam Masa →

The second value changes the ending. Use "" for no ending.

truncatewords

Cuts text to a number of words, and adds ....

Use it when: you want a short preview that does not cut a word in half, like blog post excerpts.

Input
{{ "Organic Assam Masala Chai with Cardamom" | truncatewords: 3 }}
Output
Organic Assam Masala...

slice

Takes part of a text (or part of a list). First number = where to start (0 is the first letter). Second number = how many. A negative start counts from the end.

Use it when: you need initials, a short code, or the first few items of a list.

Input
{{ "Liquid" | slice: 0 }}
{{ "Liquid" | slice: 2, 4 }}
{{ customer.first_name | slice: 0 }}{{ customer.last_name | slice: 0 }}
Output
L
quid
SH

size

Tells you how many characters in a text, or how many items in a list.

Use it when: you check length ("is the title too long?") or count items ("3 products").

Input
{{ "Masala Chai" | size }}
{{ collection.products | size }}
{{ collection.products.size }}
Output
11
3
3

You can also write .size after a name, like the last line. It works inside if too: {% if product.images.size > 1 %}.

split

Breaks a text into a list, cutting at the separator you choose.

Use it when: you need a list but only have text, for example a comma-separated setting like "S, M, L, XL". It is the only way to make a list in plain Liquid.

Input
{% assign sizes = "S, M, L, XL" | split: ", " %}
{% for size in sizes %}<button>{{ size }}</button>{% endfor %}
Output
<button>S</button><button>M</button><button>L</button><button>XL</button>

escape

Turns special HTML characters like <, >, & and quotes into safe codes.

Use it when: you print text that a shopper or merchant typed into an HTML attribute (like alt or title), so a quote in the text cannot break your HTML.

Input
{% assign note = 'Tom said "best" chai & cake' %}
<img alt="{{ note | escape }}">
Output
<img alt="Tom said &quot;best&quot; chai &amp; cake">

Without escape, the " inside the text would end the alt attribute early and break the image tag.

escape_once

Like escape, but it does not escape twice if the text is already escaped.

Use it when: text may already contain codes like &amp;, and you do not want them to turn into &amp;amp;.

Input
{{ "1 < 2 & 3" | escape_once }}
{{ "1 &lt; 2 &amp; 3" | escape_once }}
Output
1 &lt; 2 &amp; 3
1 &lt; 2 &amp; 3

url_encode

Makes text safe to put in a link. Spaces become +, symbols like @ become codes.

Use it when: you put text into a URL, like a WhatsApp share link or a search link.

Input
https://wa.me/?text={{ "Hi! I want Masala Chai" | url_encode }}
Output
https://wa.me/?text=Hi%21+I+want+Masala+Chai

url_decode

The opposite: turns link codes back into normal text.

Use it when: you read text from a URL and want to show it nicely.

Input
{{ "Masala+Chai%21" | url_decode }}
Output
Masala Chai!

Filters: numbers

plus

Adds a number.

Use it when: you add up values, like a price plus a gift-wrap charge, or a counter.

Input
{{ 4 | plus: 2 }}
{{ product.price | plus: 5000 | money }}
Output
6
₹300.00

minus

Subtracts a number.

Use it when: you work out a saving, or how much is left.

Input
{{ product.compare_at_price | minus: product.price | money }}
Output (₹300 → ₹250)
₹50.00

times

Multiplies.

Use it when: you multiply price by quantity, or turn a decimal into a percentage.

Input
{{ 3 | times: 2 }}
{{ 0.15 | times: 100 }}
Output
6
15.0

divided_by

Divides. Careful: if both numbers are whole numbers, the answer is rounded down to a whole number. To keep decimals, divide by a decimal like 3.0.

Use it when: you work out a percentage or an average.

Input
{{ 16 | divided_by: 4 }}
{{ 5 | divided_by: 3 }}
{{ 5 | divided_by: 3.0 }}
Output
4
1
1.6666666666666667

The classic use, a discount percentage. Multiply by 100 before dividing, or you get 0:

Input
{% assign saving = product.compare_at_price | minus: product.price %}
{{ saving | times: 100 | divided_by: product.compare_at_price }}% off
Output (₹300 → ₹250)
16% off

modulo

Gives the remainder after dividing. 7 divided by 3 is 2, remainder 1.

Use it when: you do something every Nth time, like a banner after every 4th product, or "odd/even" rows.

Input
{% for product in collection.products %}
  {{ product.title }}
  {% assign r = forloop.index | modulo: 4 %}
  {% if r == 0 %}<div class="promo-banner">Free delivery on all teas</div>{% endif %}
{% endfor %}

round

Rounds to the nearest whole number, or to a number of decimal places.

Use it when: you show ratings (4.37 → 4.4) or clean up maths results.

Input
{{ 2.7 | round }}
{{ 4.37 | round: 1 }}
Output
3
4.4

ceil

Rounds up to the next whole number.

Use it when: you need "at least": pages needed, boxes needed, full stars.

Input
{{ 1.2 | ceil }}
{{ 25 | divided_by: 10.0 | ceil }} pages
Output
2
3 pages

floor

Rounds down to the whole number.

Use it when: you show full stars for a rating (4.7 → 4 full stars).

Input
{{ 4.7 | floor }}
Output
4

abs

Removes the minus sign (gives the absolute value).

Use it when: you show a difference and only care how big it is, not which way.

Input
{{ -17 | abs }}
{{ 4 | abs }}
Output
17
4

at_least

Makes sure a number is not lower than a minimum.

Use it when: a value must never go below a limit, like a quantity of at least 1.

Input
{{ 0 | at_least: 1 }}
{{ 4 | at_least: 1 }}
Output
1
4

at_most

Makes sure a number is not higher than a maximum.

Use it when: you cap a value, like "show at most 10 left" or a 5-star rating.

Input
{{ 25 | at_most: 10 }}
{{ 3 | at_most: 10 }}
Output
10
3

Filters: lists

These filters work on lists (arrays), like collection.products, product.tags or a list you made with split.

first and last

Give the first or last item of a list.

Use it when: you need one item, like the first image or the newest tag. You can also write .first and .last.

Input
{{ product.tags | first }}
{{ product.images.first | image_url: width: 300 | image_tag }}
{{ "S, M, L" | split: ", " | last }}
Output
organic
<img src="…/masala-chai.jpg?width=300" …>
L

join

Joins a list into one text, with a separator you choose between items.

Use it when: you print a list in a sentence: "Tags: organic, assam, bestseller".

Input
Tags: {{ product.tags | join: ", " }}
Output
Tags: organic, assam, bestseller

map

Takes one field from every item and makes a new list of just those values. Like taking only the names from a class register.

Use it when: you need, for example, all the titles or all the vendors from a list of products.

Input
{{ collection.products | map: "title" | join: ", " }}
Output
Masala Chai, Green Tea, Clay Cup

where

Keeps only the items that match. Like a sieve.

Use it when: you want, for example, only in-stock products, or only products of one type.

Input
{% assign teas = collection.products | where: "type", "Tea" %}
{% assign in_stock = collection.products | where: "available" %}
Teas: {{ teas | map: "title" | join: ", " }}
In stock: {{ in_stock.size }}
Output
Teas: Masala Chai, Green Tea
In stock: 2

With one value (where: "available"), it keeps items where that field is "yes".

sort

Puts a list in order. Capital letters come before small letters ("Zebra" before "apple").

Use it when: you sort by a field, like price: sort: "price".

Input
{% assign cheapest_first = collection.products | sort: "price" %}
{{ cheapest_first | map: "title" | join: ", " }}
Output
Clay Cup, Green Tea, Masala Chai

sort_natural

Sorts text A to Z, ignoring capital letters. This is the order people expect.

Use it when: you sort names or titles for people to read.

Input
{% assign names = "zebra, octopus, giraffe, Sally Snake" | split: ", " %}
sort: {{ names | sort | join: ", " }}
sort_natural: {{ names | sort_natural | join: ", " }}
Output
sort: Sally Snake, giraffe, octopus, zebra
sort_natural: giraffe, octopus, Sally Snake, zebra

See the difference? sort puts every capital letter first. sort_natural reads like a dictionary.

reverse

Flips a list: last becomes first.

Use it when: you want the opposite order, like newest first. To reverse text, split it into letters first.

Input
{{ "1, 2, 3" | split: ", " | reverse | join: ", " }}
{{ "chai" | split: "" | reverse | join: "" }}
Output
3, 2, 1
iahc

uniq

Removes duplicates, keeping one of each.

Use it when: you collect values from many products (like all vendors) and each should show once.

Input
{{ collection.products | map: "vendor" | uniq | join: ", " }}
Output
Chai Corner, Clay Crafts

compact

Removes empty (nil) items from a list.

Use it when: you map a field that some items do not have, and you do not want blank spots.

Input
{% assign images = collection.products | map: "featured_image" | compact %}
{{ images.size }} products have a photo.
Output
2 products have a photo.

concat

Joins two lists into one.

Use it when: you combine two collections or two tag lists into one list to loop over.

Input
{% assign teas = "Masala, Green" | split: ", " %}
{% assign extras = "Cup, Kettle" | split: ", " %}
{{ teas | concat: extras | join: ", " }}
Output
Masala, Green, Cup, Kettle

sum

Adds up the numbers in a list, or one number field from every item.

Use it when: you total something: the quantity of all cart items, or the weight.

Input
{{ cart.items | sum: "quantity" }} items in your cart
Output
4 items in your cart

Filters: other

date

Shows a date and time in the format you choose, using codes like %d (day), %B (month name), %Y (year). "now" means the current date and time.

Use it when: you show when a blog post was published, an order date, or the current year in the footer.

Input
{{ article.published_at | date: "%d %B %Y" }}
© {{ "now" | date: "%Y" }} {{ shop.name }}
Output
17 July 2026
© 2026 Chai Corner
CodeMeansExample
%dday of month07
%B / %bmonth name / shortOctober / Oct
%Y / %yyear / short year2026 / 26
%A / %aweekday / shortWednesday / Wed
%H:%Mhours:minutes (24h)14:30
%I:%M %phours:minutes (12h)02:30 PM

default

Shows a backup value when the real value is nil, false or empty.

Use it when: a setting or metafield might be empty, and you never want a blank space.

Input
{{ product.metafields.custom.subtitle | default: "Freshly packed every week" }}
{{ section.settings.heading | default: "Our best sellers" }}
Output (subtitle is empty)
Freshly packed every week
Our best sellers

default treats false as empty. If false is a real answer you want to print, add allow_false: true: {{ setting | default: true, allow_false: true }}.

Quick recap

  • Double curly braces print a value. Curly brace with % runs logic. The pipe | changes a value with a filter.
  • Types: text, number, true/false, nil, list. Only false and nil count as "no".
  • Decisions: if, unless, elsif, else, case/when.
  • Repeating: for with limit, offset, reversed, break, continue, and the forloop helper.
  • Files: render snippets and pass in what they need. Do not use include.
  • Saving values: assign for one value, capture for a block of text.
  • Filters: text (append, replace, truncate…), numbers (plus, times, divided_by…), lists (map, where, sort, join…), and date and default.
  • Add - inside the braces to remove extra blank lines.

Try it yourself

In a Custom Liquid section on your product page, print this one line for the current product:

MASALA CHAI · 3 tags · organic, assam, bestseller · Save 16%

Hints: upcase, size, join, and the discount percentage example from divided_by. Use closest.product instead of product if you put it in a Custom Liquid block inside Product information.

Show solution
Custom Liquid
{%- assign p = product -%}
{{ p.title | upcase }} · {{ p.tags.size }} tags · {{ p.tags | join: ", " }}
{%- if p.compare_at_price > p.price -%}
  {%- assign saving = p.compare_at_price | minus: p.price -%}
  {{ ' · Save ' }}{{ saving | times: 100 | divided_by: p.compare_at_price }}%
{%- endif -%}
Check: The preview shows the title in capitals, the tag count, the tags, and the saving (only if the product has a compare-at price).

Want to go deeper? The Theme Developer course uses Liquid in every lesson, and live classes go through it with you on your own store.