Liquid is the template language that powers Shopify themes. Originally created by Shopify in 2006, Liquid has become an open-source template language used by countless developers and platforms. Understanding Liquid is essential for anyone looking to customize Shopify themes beyond the capabilities of the visual theme editor.
For merchants starting their e-commerce journey, Liquid provides the flexibility to create unique shopping experiences without learning a completely new technology stack. The language is designed to be approachable--HTML developers can quickly understand its syntax and begin making meaningful changes to their stores.
However, as store requirements grow more sophisticated, Liquid's limitations become apparent. Complex interactions, performance optimization at scale, and integration with external systems often require stepping outside the Liquid ecosystem entirely. This guide covers Liquid comprehensively while helping you understand when custom solutions might serve your business better.
What Makes Liquid Unique
Liquid occupies a specific niche in the templating world--it is neither a full programming language nor simple string replacement. This deliberate design choice makes Liquid safe to run on Shopify's servers while providing enough power to build dynamic e-commerce experiences. The language cannot access the file system, make network requests, or execute arbitrary code, which is why it runs safely on Shopify's platform.
The trade-off for this security is flexibility. Complex business logic that would be straightforward in other languages requires creative workarounds in Liquid or migration to Shopify's APIs and webhooks. For stores with straightforward customization needs, Liquid provides everything necessary. For stores requiring complex integrations, advanced personalization, or high-performance requirements, exploring Shopify Hydrogen or headless approaches may be more appropriate.
Everything you need to know to work with Liquid templating
Output Markup
Double curly braces {{ }} for displaying dynamic content with filter chaining
Tag Markup
Logic and iteration tags {% %} for control flow and variable assignment
Objects
Product, collection, cart, customer, and global objects for data access
Filters
Transform output with string, array, URL, and money filters
Sections
Modular, configurable components with JSON schema settings
Snippets
Reusable code fragments for DRY theme development
Liquid Syntax Fundamentals
The Liquid language consists of three primary constructs: output markup, tag markup, and literal text. Understanding these three elements--and how they interact--is the foundation for all Liquid development.
Output Markup
Output markup displays dynamic content on your pages. Any content wrapped in double curly braces {{ }} is evaluated as output markup. The system processes what's inside the braces and renders the result in place. For example, {{ product.title }} outputs the title of the current product being viewed.
Output markup can include filters, which modify the output. The vertical bar | character separates the value from its filters:
{{ product.title | upcase }}
{{ product.price | times: 100 | money_with_currency }}
Tag Markup
Tag markup handles the logic, iteration, and variable assignment in Liquid templates. Tags are enclosed in curly brace percentage signs: {% %}. A conditional statement might look like {% if product.available %}, which controls what renders based on product availability.
Tags fall into several categories that serve distinct purposes in building dynamic templates:
Control flow tags like if, unless, and elsif execute code based on conditions. Iteration tags like for and while repeat content for arrays. Variable tags like assign, capture, and increment create and modify data. Theme tags like section and render include template content. Each category handles a specific aspect of template logic.
Whitespace Control
By adding a hyphen to the start or end of a tag, you strip surrounding whitespace. This prevents unwanted spaces from appearing in your rendered output:
{{- product.title -}} {# removes whitespace before and after #}
{{- product.title }} {# removes only preceding whitespace #}
{%- if condition -%} {# removes whitespace around the entire block #}
In practice, whitespace control is essential for clean HTML output. Compare these examples:
{# Without whitespace control - produces unwanted spaces #}
<div>
{{ product.title }}
</div>
{# With whitespace control - clean output #}
<div>
{{- product.title -}}
</div>
Working with Objects
Objects are the data containers in Liquid. They represent the various entities in a Shopify store--products, collections, customers, orders, and more. Objects contain properties (called attributes) that you access using dot notation.
Global Objects
Every Liquid template has access to a set of global objects that represent the current context:
| Object | Purpose |
|---|---|
template | Identifies which template is currently rendering |
settings | Access to theme editor settings |
content_for_header | Dynamic header content |
content_for_layout | Main template content |
theme | Current theme information |
request | HTTP request details |
The settings object provides access to theme editor settings defined in your theme's schema. A theme might expose a color picker that stores its value in settings, which Liquid then accesses: {{ settings.primary_color }}. This enables merchants to customize their stores through the visual editor without touching code.
Product and Collection Objects
The product object represents the currently viewed product. Its attributes include:
{{ product.title }} {# Product name #}
{{ product.description }} {# Full HTML description #}
{{ product.price }} {# Price in cents #}
{{ product.featured_image | img_url: 'large' }} {# Main image URL #}
{{ product.vendor }} {# Vendor/brand name #}
{{ product.type }} {# Product type #}
{{ product.tags }} {# Array of tags #}
{{ product.options }} {# Available options #}
The variants attribute is particularly important--it contains an array of product variants, each with its own price, inventory, and options. Accessing variant properties requires iteration or direct index access:
{% for variant in product.variants %}
<div class="variant">
<span>{{ variant.title }}</span>
<span>{{ variant.price | money }}</span>
<span>{% if variant.available %}In Stock{% else %}Out of Stock{% endif %}</span>
</div>
{% endfor %}
The collection object represents the current collection page:
{{ collection.title }} {# Collection name #}
{{ collection.description }} {# Collection description #}
{{ collection.products }} {# Array of product objects #}
{{ collection.image | img_url: 'master' }} {# Collection banner image #}
{{ collection.all_products_count }} {# Total product count #}
Cart and Customer Objects
<p>Items: {{ cart.item_count }}</p>
<p>Total: {{ cart.total_price | money }}</p>
<p>Subtotal: {{ cart.subtotal_price | money }}</p>
{% if cart.applied_discounts.size > 0 %}
<p>Discount: {{ cart.total_discounts | money }}</p>
{% endif %}
{% if customer %}
<p>Welcome back, {{ customer.first_name }}!</p>
<p>Orders: {{ customer.orders_count }}</p>
<p>Total Spent: {{ customer.total_spent | money }}</p>
{% endif %}
For a comprehensive reference of all available objects and their attributes, consult the Shopify Liquid reference documentation.
Control Flow with Tags
Control flow tags determine what content renders based on conditions. They are fundamental to creating dynamic, data-driven templates that respond to store data and user interactions.
Conditional Statements
The if tag begins a conditional block that executes only when its condition is true. Conditions support comparison operators (==, !=, >, <, >=, <=) and logical operators (and, or, not):
{% if product.price > 100 %}
<span class="premium-badge">Premium Product</span>
{% elsif product.price > 50 %}
<span class="mid-range-badge">Mid-Range</span>
{% else %}
<span class="value-badge">Best Value</span>
{% endif %}
The unless tag works as the opposite of if--it executes when a condition is false. This is useful when checking for the absence of a condition:
{% unless customer %}
<a href="/account/login">Log in for personalized offers</a>
{% endunless %}
Complex Boolean Logic
Complex conditions combine multiple checks using and and or operators. Parentheses are not supported in Liquid conditions, so operator precedence matters--the and operator has higher precedence than or:
{# Evaluates as (featured and available) or on_sale #}
{% if product.featured and product.available or product.on_sale %}
<span class="special-badge">Featured Special</span>
{% endif %}
{# Explicitly checking multiple conditions #}
{% if product.available == false and product.tags contains 'coming-soon' %}
<p>Coming soon!</p>
{% endif %}
The Contains Operator
The contains operator checks if a string or array includes a value. It works with product tags, collection handles, and string content:
{% if product.tags contains 'sale' %}
<span class="sale-badge">On Sale!</span>
{% endif %}
{% if product.description contains 'limited edition' %}
<p>Limited edition item--order soon!</p>
{% endif %}
{% if customer.tags contains 'vip' %}
<p>VIP pricing applied!</p>
{% endif %}
Handling Empty States
Checking for empty or missing data prevents errors and improves user experience. The blank and empty literals help with these checks--blank is true for empty strings, nil, or empty arrays; empty specifically checks array length:
{% if collection.products != blank %}
<div class="product-grid">
{% for product in collection.products %}
{% render 'product-card', product: product %}
{% endfor %}
</div>
{% else %}
<div class="empty-collection">
<p>No products found in this collection.</p>
<a href="/collections/all">Browse all products</a>
</div>
{% endif %}
{% if customer.default_address == blank %}
<p>Please add a shipping address to your account.</p>
{% endif %}
Iteration and Loops
Iteration tags repeat content for each item in an array or hash. The for tag is the primary iteration tool, with options to control iteration behavior and limit results.
Basic For Loops
A for loop iterates over any array, executing the block for each item. The loop object provides special variables for tracking position and controlling layout:
<div class="product-grid">
{% for product in collection.products limit: 8 %}
<div class="product-card">
{% render 'product-image', image: product.featured_image %}
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</div>
{% endfor %}
</div>
Loop Properties
| Property | Description |
|---|---|
forloop.index | Current iteration (1-based) |
forloop.index0 | Current iteration (0-based) |
forloop.first | True for first iteration |
forloop.last | True for last iteration |
forloop.rindex | Remaining iterations (1-based) |
forloop.rindex0 | Remaining iterations (0-based) |
{# Add classes for first and last items #}
{% for product in collection.products %}
<div class="product-card {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %}">
{{ product.title }}
</div>
{% endfor %}
Pagination with Offset and Limit
The offset parameter starts iteration from a specific position, while limit constrains the number of iterations. These parameters create pagination patterns:
{# Page 1: products 1-4 #}
{% for product in collection.products offset: 0 limit: 4 %}
{% render 'product-card', product: product %}
{% endfor %}
{# Page 2: products 5-8 #}
{% for product in collection.products offset: 4 limit: 4 %}
{% render 'product-card', product: product %}
{% endfor %}
{# Page 3: products 9-12 #}
{% for product in collection.products offset: 8 limit: 4 %}
{% render 'product-card', product: product %}
{% endfor %}
The reversed parameter reverses the iteration order without needing to modify your data:
{% for product in collection.products reversed limit: 4 %}
{% render 'product-card', product: product %}
{% endfor %}
Break and Continue
The break tag exits the loop immediately, while continue skips to the next iteration. These are useful for stopping iteration when conditions are met or skipping unwanted items:
{% for variant in product.variants %}
{% unless variant.available %}
{% continue %}
{% endunless %}
{% render 'variant-option', variant: variant %}
{% endfor %}
{% for product in collection.products %}
{% if product.price > 1000 %}
{% break %}
{% endif %}
{% render 'product-card', product: product %}
{% endfor %}
Filters Reference
Filters transform Liquid output. They are appended to output markup with the | character and can chain together. Shopify provides extensive built-in filters for common transformations.
String Filters
String filters manipulate text content. The upcase and downcase change case, capitalize capitalizes the first letter, append and prepend add text, and strip_html removes HTML tags:
{{ product.title | upcase }}
{{ product.title | downcase }}
{{ product.title | capitalize }}
{{ collection.handle | append: '-sale' }}
{{ 'sale-' | prepend: collection.handle }}
{{ product.description | strip_html }}
{{ product.description | truncate: 100 }}
{{ product.description | truncatewords: 20 }}
Array Filters
Array filters work with lists of values. join combines elements with a separator, first and last extract elements, map creates an array from a property, and sort orders elements:
{% assign product_titles = collection.products | map: 'title' %}
{% assign available_products = collection.products | where: 'available', true %}
{% assign sorted_products = collection.products | sort: 'price' %}
{% assign all_tags = collection.products | map: 'tags' | uniq | sort %}
{{ product_titles | join: ', ' }}
{{ sorted_products.first.title }}
URL and Asset Filters
URL filters handle link and asset paths. The img_url filter produces optimized image URLs with size parameters:
{{ product.featured_image | img_url: 'large' }}
{{ product.featured_image | img_url: '600x600', crop: 'center' }}
{{ product.featured_image | img_url: '800x800', scale: 2, format: 'webp' }}
{{ image | img_url: 'original' }}
Money and Number Filters
Money filters format currency values for display. Number filters handle numeric rounding:
{{ product.price | money }}
{{ product.price | money_with_currency }}
{{ product.price | money_without_currency }}
{{ product.price | divided_by: 100 | round }}
{{ product.price | times: 1.1 | money }}
{{ product.price | ceil }}
{{ product.price | floor }}
Common Filter Chains
Filters chain together, with each receiving the output of the previous one:
{# Format price with percentage calculation #}
{{ product.price | times: 1.13 | money_with_currency }}
{# Strip HTML and truncate description #}
{{ product.description | strip_html | truncate: 150 }}
{# Format and handle missing values #}
{{ product.compare_at_price | default: product.price | money }}
{# Handle optional images safely #}
{% assign img = product.featured_image | default: false %}
{% if img %}
<img src="{{ img | img_url: 'medium' }}" alt="{{ product.title }}">
{% endif %}
For the complete filter reference, see the Shopify Liquid filters documentation.
Building with Sections
Sections are modular, configurable template components that merchants can add, remove, and customize through the theme editor. They represent Shopify's recommended approach to theme architecture, replacing older methods of template customization.
Section Structure
A section consists of a Liquid file in the sections/ directory and a JSON schema defining its settings:
<section class="featured-collection" {{ section.shopify_attributes }}>
<h2>{{ section.settings.title }}</h2>
<div class="collection-grid">
{% for product in section.settings.collection.products limit: 4 %}
{% render 'product-card', product: product %}
{% endfor %}
</div>
</section>
{% schema %}
{
"name": "Featured Collection",
"settings": [
{
"type": "text",
"id": "title",
"label": "Heading",
"default": "Featured Products"
},
{
"type": "collection",
"id": "collection",
"label": "Collection"
},
{
"type": "color",
"id": "heading_color",
"label": "Heading Color",
"default": "#000000"
}
],
"presets": [
{
"name": "Featured Collection"
}
]
}
{% endschema %}
Schema Settings Types
Shopify provides numerous setting types for section configuration:
| Setting Type | Purpose | Example |
|---|---|---|
text | Single-line text input | Section heading |
textarea | Multi-line text input | Section description |
range | Number slider | Items to display |
color | Color picker | Background colors |
select | Dropdown selection | Layout style |
radio | Single-choice buttons | Alignment options |
checkbox | Toggle switch | Show/hide elements |
collection | Collection selector | Featured collection |
product | Product selector | Featured product |
image_picker | Image upload | Custom banners |
video | Video selection | Promo videos |
Blocks within Sections
Blocks provide finer granularity within sections, allowing merchants to add, remove, and reorder content items. Each block has its own schema:
<section class="image-banner" {{ section.shopify_attributes }}>
<div class="banner-content">
{% for block in section.blocks %}
{% case block.type %}
{% when 'heading' %}
<h2 {{ block.shopify_attributes }}>{{ block.settings.heading }}</h2>
{% when 'text' %}
<p {{ block.shopify_attributes }}>{{ block.settings.text }}</p>
{% when 'button' %}
<a href="{{ block.settings.url }}" {{ block.shopify_attributes }}>
{{ block.settings.label }}
</a>
{% endcase %}
{% endfor %}
</div>
</section>
{% schema %}
{
"name": "Image Banner",
"blocks": [
{
"type": "heading",
"name": "Heading",
"settings": [
{ "type": "text", "id": "heading", "label": "Heading text" }
]
},
{
"type": "button",
"name": "Button",
"settings": [
{ "type": "text", "id": "label", "label": "Button label" },
{ "type": "url", "id": "url", "label": "Button link" }
]
}
],
"presets": [{ "name": "Image Banner" }]
}
{% endschema %}
Section Best Practices
When building sections, follow these guidelines for maintainable themes:
- Use
{{ section.shopify_attributes }}in your section wrapper for proper Shopify integration - Access settings through
section.settings.setting_id - Include sensible defaults in schema definitions
- Test sections with different setting combinations
- Use blocks for repeatable content rather than hardcoding iterations
- Consider responsive behavior when building layout sections
For merchants requiring extensive customization, understanding Shopify Theme Development principles is essential for creating effective sections.
Creating Reusable Snippets
Snippets are reusable code fragments stored in the snippets/ directory. They enable DRY (Don't Repeat Yourself) coding by centralizing frequently used markup patterns. A product card, for instance, might appear in multiple contexts--collection grids, search results, and related product sections.
Creating a Snippet
Snippets are named files with .liquid extension. They accept parameters through the render tag, which passes data to the snippet:
{% comment %}
Product Card Snippet
Accepts: product, show_secondary_price (optional)
{% endcomment %}
<div class="product-card">
<a href="{{ product.url }}">
{% if product.featured_image %}
<img src="{{ product.featured_image | img_url: 'medium' }}"
alt="{{ product.featured_image.alt | default: product.title }}"
loading="lazy">
{% else %}
{{ 'product-1' | placeholder_svg_tag }}
{% endif %}
<h3>{{ product.title }}</h3>
<p class="price">{{ product.price | money }}</p>
{% if show_secondary_price and product.compare_at_price > product.price %}
<p class="original-price">{{ product.compare_at_price | money }}</p>
{% endif %}
</a>
</div>
Rendering Snippets
The render tag includes snippets and passes variables. Unlike the deprecated include tag, render does not have access to the parent template's variables unless explicitly passed:
{% render 'product-card', product: collection.products[0] %}
{% render 'product-card', product: related_product, show_secondary_price: true %}
{# Loop through products and render card for each #}
{% for product in collection.products limit: 4 %}
{% render 'product-card', product: product %}
{% endfor %}
Sections vs Snippets: When to Use Each
Understanding when to use sections versus snippets is crucial for proper theme architecture:
| Aspect | Sections | Snippets |
|---|---|---|
| User Configurable | Yes, via theme editor | No |
| Schema/Presets | Yes | No |
| Performance | Load with {% section %} | Load with {% render %} |
| Blocks Support | Yes | No |
| Best For | User-editable components | Pure markup patterns |
Use sections when:
- Merchants need to customize content through the theme editor
- You want to offer preset configurations
- Content blocks need to be reorderable
- Configuration options should persist across theme updates
Use snippets when:
- You need reusable markup without user configuration
- A component appears in many places
- You're extracting repetitive code patterns
- Performance is critical (render is more efficient than section)
Common Snippet Patterns
Snippets excel at reusable UI components:
{# Newsletter form snippet #}
{% form 'customer' %}
<input type="email" name="email" placeholder="Enter your email">
<button type="submit">Subscribe</button>
{% endform %}
{# Icon snippet #}
<svg class="icon icon-{{ name }}" aria-hidden="true">
<use xlink:href="#icon-{{ name }}"></use>
</svg>
{# Social links snippet #}
<div class="social-links">
{% for link in social_links %}
<a href="{{ link.url }}" aria-label="{{ link.name }}">
{% render 'icon', name: link.icon %}
</a>
{% endfor %}
</div>
Theme Architecture Best Practices
Well-structured Shopify themes follow consistent patterns that improve maintainability, performance, and collaboration. Understanding these patterns helps developers create themes that scale and are easy to maintain.
Directory Structure
Modern themes use a clear directory structure:
/assets/ # CSS (.css, .scss), JavaScript (.js), images
/layout/ # theme.liquid and alternate layouts
└── theme.liquid # Main template wrapper
/sections/ # Configurable section files (.liquid)
├── header.liquid
├── footer.liquid
└── featured-collection.liquid
/snippets/ # Reusable code fragments (.liquid)
├── product-card.liquid
├── icon.liquid
└── newsletter-form.liquid
/templates/ # Product, collection, page templates
├── product.json
├── collection.json
├── index.json
└── blog.json
/config/ # Theme settings and data files
locales/ # Translation files (.json)
templates/ # Legacy template files
This organization separates concerns and makes theme files discoverable. The templates/ directory holds JSON templates for Online Store 2.0, while templates/ (root) contains legacy liquid templates for backward compatibility.
Performance Optimization
Liquid template rendering impacts page load times. Follow these practices for optimal performance:
Minimize render calls within loops by assigning to variables first:
{% comment %} Good: load once, use multiple times {% endcomment %}
{% assign featured_products = collection.products | limit: 8 %}
{% for product in featured_products %}
{% render 'product-card', product: product %}
{% endfor %}
{% comment %} Avoid: render inside loop loads snippet each iteration {% endcomment %}
{% for product in collection.products limit: 8 %}
{% render 'product-card', product: product %}
{% endfor %}
Use specific image sizes rather than loading full images:
{# Good: request specific size #}
<img src="{{ product.featured_image | img_url: 'medium' }}" loading="lazy">
{# Avoid: loading original image #}
<img src="{{ product.featured_image | img_url: 'original' }}" loading="lazy">
Limit products loaded in collection grids:
{% assign displayed_products = collection.products | limit: 12 | offset: 0 %}
Error Handling and Debugging
Liquid provides limited debugging capabilities. Use these techniques during development:
- The
echotag (equivalent to{{ }}) helps verify values - Comment tags
{% comment %}disable blocks for testing - Use
{{ variable | default: 'fallback' }}to handle undefined values - The Shopify Theme Check extension identifies errors and suggests improvements
Performance Checklist
Before deploying a theme, verify these performance considerations:
- Image URLs use specific sizes (not 'original')
- Product loops use
limitto prevent over-fetching - Snippets are rendered efficiently (not inside loops when possible)
- Lazy loading attributes on below-fold images
- JavaScript deferred or loaded asynchronously
- CSS minified and consolidated
- No unused sections or snippets in production
For stores requiring advanced performance optimization or custom functionality beyond Liquid's capabilities, consider exploring Shopify Hydrogen for headless implementations.
Frequently Asked Questions
Sources
- Shopify Dev: Liquid Reference - Official Shopify documentation providing authoritative reference for all Liquid tags, filters, and objects
- Viha Digital Commerce: Shopify Liquid Code Guide 2025 - Industry tutorial covering practical implementation with code examples