Liquid Fundamentals for Theme Developers Beginner 12 min read

Liquid Syntax: Tags, Objects, and Filters

Learn the three fundamental building blocks of Liquid templating: output tags for displaying data, logic tags for control flow, and filters for transforming values.

Liquid has three core syntax elements that you’ll use in every theme you build. Understanding these building blocks is essential before diving into more complex patterns. Let’s break down each one.

The Three Types of Liquid Syntax

Every piece of Liquid code falls into one of three categories:

  1. Objects (output tags): {{ }} displays data
  2. Tags (logic tags): {% %} performs logic without output
  3. Filters: | transforms data before output
{{ product.title | upcase }}
{% if product.available %}
In stock!
{% endif %}

Output Tags: Displaying Data with {{ }}

Output tags display the value of an object or variable. They always produce visible output on the page.

{{ shop.name }}
{{ product.title }}
{{ cart.total_price | money }}

When Output Tags Produce Nothing

If the value is nil or doesn’t exist, the output tag produces an empty string (no error, just nothing appears):

{{ product.nonexistent_property }}
{# Outputs nothing, no error #}

Output Inside HTML Attributes

You can use output tags anywhere in your HTML:

<a href="{{ product.url }}" title="{{ product.title | escape }}">
<img src="{{ product.featured_image | image_url: width: 400 }}"
alt="{{ product.featured_image.alt | escape }}">
</a>

Important: Always use the escape filter when outputting user-generated content inside HTML attributes to prevent XSS vulnerabilities.

Logic Tags: Control Flow with {% %}

Logic tags perform operations without producing visible output. They control what gets rendered and how.

Common Logic Tags

{# Conditionals #}
{% if product.available %}
Add to cart
{% endif %}
{# Loops #}
{% for product in collection.products %}
{{ product.title }}
{% endfor %}
{# Variable assignment #}
{% assign greeting = "Hello" %}
{# Capturing output into a variable #}
{% capture full_title %}
{{ product.title }} by {{ product.vendor }}
{% endcapture %}

Logic Tags Never Output Directly

The tag itself produces no output. Only output tags {{ }} or HTML within the tag block will appear:

{% assign x = 5 %}
{# Nothing appears on the page from this line #}
The value is: {{ x }}
{# This line outputs: "The value is: 5" #}

Filters: Transforming Data with |

Filters modify the output of objects and variables. They’re applied using the pipe character | and can be chained together.

Basic Filter Syntax

{{ "hello world" | capitalize }}
{# Output: Hello world #}
{{ product.title | upcase }}
{# Output: AWESOME T-SHIRT #}
{{ product.price | money }}
{# Output: $29.99 #}

Filters with Arguments

Some filters accept arguments, passed after a colon:

{{ product.title | truncate: 20 }}
{# Truncates to 20 characters #}
{{ "hello" | append: " world" }}
{# Output: hello world #}
{{ product.featured_image | image_url: width: 400, height: 300 }}
{# Multiple named arguments #}

Filter Chaining

You can chain multiple filters together. They execute left to right:

{{ product.title | upcase | truncate: 15 }}
{# First uppercases, then truncates #}
{{ " hello world " | strip | capitalize | append: "!" }}
{# Output: Hello world! #}

Order matters! Compare these two examples:

{{ "hello world" | truncate: 8 | upcase }}
{# Output: HELLO... #}
{{ "hello world" | upcase | truncate: 8 }}
{# Output: HELLO... #}
{# Same result here, but not always! #}
{{ "hello world" | capitalize | upcase }}
{# Output: HELLO WORLD #}
{{ "hello world" | upcase | capitalize }}
{# Output: Hello world #}
{# Different! capitalize lowercases everything except first letter #}

Essential Filters by Category

String Filters:

{{ "hello" | upcase }} {# HELLO #}
{{ "HELLO" | downcase }} {# hello #}
{{ "hello" | capitalize }} {# Hello #}
{{ " hello " | strip }} {# hello #}
{{ "hello" | size }} {# 5 #}
{{ "hello" | append: " world" }} {# hello world #}
{{ "hello" | prepend: "say " }} {# say hello #}
{{ "hello" | replace: "l", "L" }} {# heLLo #}

Number Filters:

{{ 16 | plus: 4 }} {# 20 #}
{{ 16 | minus: 4 }} {# 12 #}
{{ 16 | times: 2 }} {# 32 #}
{{ 16 | divided_by: 4 }} {# 4 #}
{{ 4.5 | round }} {# 5 #}
{{ 4.5 | floor }} {# 4 #}
{{ 4.2 | ceil }} {# 5 #}

Money Filters:

{{ product.price | money }} {# $19.99 #}
{{ product.price | money_with_currency }} {# $19.99 USD #}
{{ product.price | money_without_trailing_zeros }} {# $20 instead of $20.00 #}

Array Filters:

{{ collection.products | size }} {# 24 #}
{{ collection.products | first }} {# First product #}
{{ collection.products | last }} {# Last product #}
{{ collection.products | map: "title" }} {# Array of titles #}
{{ collection.products | where: "available" }} {# Only available products #}
{{ collection.products | sort: "price" }} {# Sorted by price #}

HTML/URL Filters:

{{ "Bold text" | escape }} {# Escapes HTML entities #}
{{ product.description | strip_html }} {# Removes HTML tags #}
{{ "style.css" | asset_url }} {# Full asset URL #}
{{ product | url_for_type: product.type }} {# URL for type #}

Whitespace Control

By default, Liquid tags add whitespace to your HTML output. This can result in messy HTML with unexpected blank lines and spaces. Use hyphens to strip whitespace.

Seeing the Problem

Here’s a common scenario. This Liquid code:

<p>
{% if customer %}
Welcome back!
{% endif %}
</p>

Produces this HTML output:

<p>Welcome back!</p>

Notice the blank lines? The {% if %} and {% endif %} tags are replaced with empty lines.

The Solution: Hyphen Syntax

Add hyphens inside your tags to strip whitespace:

  • {%- strips whitespace before the tag
  • -%} strips whitespace after the tag
  • {{- strips whitespace before the output
  • -}} strips whitespace after the output

The same code with whitespace control:

<p>
{%- if customer -%}
Welcome back!
{%- endif -%}
</p>

Produces clean HTML:

<p>Welcome back!</p>

One-Sided Whitespace Control

You don’t have to use hyphens on both sides. Use them only where needed:

{# Strip only AFTER the tag #}
{% assign name = "World" -%}
Hello
{# Strip only BEFORE the tag #}
World
{%- assign greeting = "Hello" %}

This is useful when you want to preserve spacing on one side:

<p>Price: {% if on_sale -%} <strong>{{ sale_price }}</strong> {%- else -%} {{ price }} {%- endif %}</p>

HTML output:

<p>Price: <strong>$19.99</strong></p>

Here’s a breakdown of each hyphen:

  • {% if on_sale -%} strips after, so no space before <strong>
  • {%- else -%} strips both sides
  • {%- endif %} strips before, so no trailing space

Compare to using hyphens on both sides of everything:

<p>Price:{%- if on_sale -%}<strong>{{ sale_price }}</strong>{%- else -%}{{ price }}{%- endif -%}</p>

HTML output:

<p>Price:<strong>$19.99</strong></p>

Notice “Price:” now has no space after it, which looks wrong.

Loop Example: Before and After

Without whitespace control:

<ul>
{% for tag in product.tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>

HTML output (with product tags “cotton”, “summer”, “sale”):

<ul>
<li>cotton</li>
<li>summer</li>
<li>sale</li>
</ul>

With whitespace control:

<ul>
{%- for tag in product.tags -%}
<li>{{ tag }}</li>
{%- endfor -%}
</ul>

Clean HTML output:

<ul>
<li>cotton</li>
<li>summer</li>
<li>sale</li>
</ul>

Inline Elements: Where It Really Matters

Whitespace is especially important with inline elements. Extra spaces become visible:

Without control:

<span class="price">
{% if on_sale %}
{{ sale_price | money }}
{% else %}
{{ regular_price | money }}
{% endif %}
</span>

HTML output:

<span class="price"> $19.99 </span>

This creates visible gaps around the price text!

With control:

<span class="price">
{%- if on_sale -%}
{{- sale_price | money -}}
{%- else -%}
{{- regular_price | money -}}
{%- endif -%}
</span>

Clean output:

<span class="price">$19.99</span>

When to Use Whitespace Control

Always use on:

  • Logic tags inside inline elements (<span>, <a>, <button>)
  • Tags in loops to prevent blank line buildup
  • Variable assignments that shouldn’t affect layout

Be careful with:

  • Output tags where you want natural spacing
  • Tags around block elements (often unnecessary)
{# These preserve natural spacing around the name #}
Hello, {{ customer.first_name }}!
{# This removes ALL surrounding whitespace #}
Hello,{{- customer.first_name -}}!
{# Output: Hello,Jane! (no space before name, no space before !) #}

Best Practice Pattern

A common pattern is to use whitespace control on logic tags but leave output tags alone:

<ul>
{%- for item in collection.products -%}
<li>{{ item.title }}</li>
{%- endfor -%}
</ul>

This keeps your loops clean while preserving natural text spacing.

Comments

Liquid supports comments that won’t appear in the rendered HTML:

{% comment %}
This is a multi-line comment.
It won't appear in the page source.
{% endcomment %}
{# This is an inline comment (Shopify-specific) #}

Practical Example: Product Card

Let’s combine everything into a real-world example:

{%- comment -%}
Product Card Component
Displays a product with image, title, price, and availability
{%- endcomment -%}
<article class="product-card">
{%- if product.featured_image -%}
<a href="{{ product.url }}">
<img
src="{{ product.featured_image | image_url: width: 400 }}"
alt="{{ product.featured_image.alt | default: product.title | escape }}"
loading="lazy"
width="400"
height="{{ 400 | divided_by: product.featured_image.aspect_ratio | round }}"
>
</a>
{%- endif -%}
<div class="product-card__info">
<h3 class="product-card__title">
<a href="{{ product.url }}">
{{ product.title | truncate: 50 }}
</a>
</h3>
<p class="product-card__vendor">
{{ product.vendor }}
</p>
<p class="product-card__price">
{%- if product.compare_at_price > product.price -%}
<span class="price--sale">{{ product.price | money }}</span>
<span class="price--compare">{{ product.compare_at_price | money }}</span>
{%- else -%}
{{ product.price | money }}
{%- endif -%}
</p>
{%- unless product.available -%}
<span class="badge badge--sold-out">Sold Out</span>
{%- endunless -%}
</div>
</article>

Practice Exercise

Create a collection header that displays:

  1. The collection title (uppercased)
  2. The collection description (if it exists)
  3. The product count with proper pluralization
  4. A “Sort by” label

Try it yourself, then check the solution:

<header class="collection-header">
<h1>{{ collection.title | upcase }}</h1>
{%- if collection.description != blank -%}
<div class="collection-description">
{{ collection.description }}
</div>
{%- endif -%}
<p class="collection-count">
{%- assign count = collection.products_count -%}
{{ count }} product{%- if count != 1 -%}s{%- endif -%}
</p>
<p class="collection-sort">
Sort by: {{ collection.sort_by | default: "Best Selling" | capitalize }}
</p>
</header>

Try it live: Experiment with filters, conditionals, and whitespace control in our Liquid Playground with pre-loaded Shopify data.

Key Takeaways

  1. Output tags {{ }} display values on the page
  2. Logic tags {% %} control flow without producing output
  3. Filters | transform values before display
  4. Filter order matters when chaining multiple filters
  5. Use whitespace control ({%- and -%}) to keep HTML clean
  6. Always escape user-generated content in HTML attributes

What’s Next?

Now that you understand Liquid’s core syntax, the next lesson covers Scope and how different types of settings and objects are available in different contexts throughout your theme.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...