Why Every Shopify Developer Needs a Liquid Cheat Sheet
With over 1.7 million retailers depending on Liquid for their storefronts, Shopify's templating language is the single most important skill in the Shopify development toolkit. Yet even experienced developers find themselves searching for that one filter name, forgetting the exact metafield access syntax, or second-guessing whether render fully replaced include.
This Shopify Liquid cheat sheet for developers is your bookmark-worthy quick reference. Every object, tag, filter, and pattern is organized so you can find what you need in seconds instead of digging through documentation tabs.
Whether you're building a custom Dawn child theme or troubleshooting a client's legacy storefront, this guide covers Liquid syntax from the fundamentals through advanced section schemas, metaobject access, and the performance patterns that separate sluggish stores from fast ones.
Liquid Fundamentals: Objects, Tags, and Filters
Liquid has three building blocks. Master these and everything else is just vocabulary.
Objects: Accessing Store Data
Objects output dynamic data and are wrapped in double curly braces. They represent everything in a Shopify store, from products to customer accounts.
{{ product.title }}
{{ cart.total_price | money }}
{{ shop.name }}The most-used global objects:
| Object | What It Returns | Common Properties |
|---|---|---|
| product | Current product | title, price, variants, images, metafields, available |
| collection | Current collection | title, products, all_products_count, sort_by |
| cart | Shopping cart | items, item_count, total_price, note |
| customer | Logged-in customer | name, email, orders, tags, addresses |
| shop | Store settings | name, url, currency, locale, metafields |
| page | CMS page | title, content, url, handle |
| request | Current request | locale, host, path |
| routes | Dynamic URL paths | root_url, account_url, cart_url |
| section | Current section | id, settings, blocks |
| block | Current block | id, type, settings, shopify_attributes |
Tags: Logic and Control Flow
Tags are wrapped in {% %} and handle all logic. They produce no visible output on their own.
{% if product.available %}
<button>Add to Cart</button>
{% else %}
<span>Sold Out</span>
{% endif %}Filters: Transforming Output
Filters modify object output using the pipe | character. You can chain multiple filters together.
{{ product.title | upcase | truncate: 30 }}
{{ product.price | money_with_currency }}
{{ 'now' | date: '%B %d, %Y' }}Control Flow Tags: Conditionals and Logic
Conditionals are the backbone of dynamic templates. Here's every control flow tag you'll use.
if / elsif / else
{% if product.type == 'T-Shirt' %}
<p>Check our size guide</p>
{% elsif product.type == 'Poster' %}
<p>Ships in a tube</p>
{% else %}
<p>Standard shipping</p>
{% endif %}unless (Inverse Conditional)
unless is syntactic sugar for "if NOT." Use it when testing a single falsy condition.
{% unless product.available %}
<span class="sold-out-badge">Sold Out</span>
{% endunless %}case / when (Switch Statement)
{% case product.vendor %}
{% when 'Nike' %}
<img src="nike-badge.png" />
{% when 'Adidas' %}
<img src="adidas-badge.png" />
{% else %}
<img src="generic-badge.png" />
{% endcase %}Operators available in conditionals:
| Operator | Meaning | Example |
|---|---|---|
| == | Equals | {% if product.type == 'Shirt' %} |
| != | Not equals | {% if cart.item_count != 0 %} |
| >, <, >=, <= | Comparison | {% if product.price > 5000 %} |
| contains | String/array includes | {% if product.tags contains 'sale' %} |
| and / or | Boolean logic | {% if customer and customer.tags contains 'vip' %} |
Iteration Tags: Loops and Cycles

Loops let you render collections of data. Shopify enforces a 50-iteration limit per for loop, so use paginate for large datasets.
for Loop with Parameters
{% for product in collection.products limit: 4 offset: 2 %}
<div class="product-card">
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</div>
{% else %}
<p>No products found.</p>
{% endfor %}Available `for` loop parameters:
- `limit` -- Maximum iterations
- `offset` -- Skip first N items
- `reversed` -- Reverse iteration order
The `forloop` helper object:
| Property | Returns |
|---|---|
| forloop.index | Current iteration (1-based) |
| forloop.index0 | Current iteration (0-based) |
| forloop.first | true on first iteration |
| forloop.last | true on last iteration |
| forloop.length | Total iterations |
cycle (Alternating Values)
Perfect for alternating CSS classes or zebra-striping rows.
{% for item in cart.items %}
<tr class="{% cycle 'row-light', 'row-dark' %}">
<td>{{ item.title }}</td>
</tr>
{% endfor %}Pagination
Use paginate to break large collections into pages. This is essential for collections with more than 50 products.
{% paginate collection.products by 12 %}
{% for product in collection.products %}
{{ product.title }}
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}Variable Tags: assign, capture, increment

Variable tags let you store and manipulate data within templates. Using them strategically is one of the best performance optimizations you can make.
assign
Creates a single variable. Use this to cache values you reference multiple times.
{% assign featured_image = product.featured_image %}
{% assign on_sale = false %}
{% if product.compare_at_price > product.price %}
{% assign on_sale = true %}
{% endif %}capture
Captures a block of rendered content into a variable. Ideal for building complex strings or HTML fragments.
{% capture product_badge %}
{% if product.available == false %}
<span class="badge badge--sold-out">Sold Out</span>
{% elsif product.compare_at_price > product.price %}
<span class="badge badge--sale">Sale</span>
{% endif %}
{% endcapture %}
{{ product_badge }}increment / decrement
Creates a counter that persists across the template. Starts at 0 and increments each time it's called.
{% increment counter %} <!-- outputs 0 -->
{% increment counter %} <!-- outputs 1 -->
{% increment counter %} <!-- outputs 2 -->Pro tip: increment and assign variables exist in separate namespaces. An assign variable named counter won't conflict with an increment counter of the same name.
The Complete Filter Reference

Filters are where Liquid's real power lives. Here's every category with the filters you'll reach for most often.
String Filters
| Filter | Example | Output |
|---|---|---|
| append | {{ 'hello' \ | append: ' world' }} |
| hello world | prepend | |
| {{ 'world' \ | prepend: 'hello ' }} | hello world |
| capitalize | {{ 'shopify' \ | capitalize }} |
| Shopify | downcase / upcase | |
| {{ 'Hello' \ | downcase }} | hello |
| strip | {{ ' padded ' \ | strip }} |
| padded | truncate | |
| {{ 'Long title here' \ | truncate: 10 }} | Long ti... |
| replace | {{ 'Hello' \ | replace: 'H', 'J' }} |
| Jello | remove | |
| {{ 'Hello' \ | remove: 'l' }} | Heo |
| split | {{ 'a,b,c' \ | split: ',' }} |
| Array [a, b, c] | url_encode | |
| {{ 'hello world' \ | url_encode }} | hello+world |
| strip_html | {{ '<b>Bold</b>' \ | strip_html }} |
| Bold | newline_to_br | |
| Converts \n to <br> | HTML line breaks | |
| escape | {{ '<script>' \ | escape }} |
Math Filters
| Filter | Example | Output |
|---|---|---|
| plus | {{ 5 \ | plus: 3 }} |
| 8 | minus | |
| {{ 10 \ | minus: 4 }} | 6 |
| times | {{ 3 \ | times: 4 }} |
| 12 | divided_by | |
| {{ 10 \ | divided_by: 3 }} | 3 |
| modulo | {{ 10 \ | modulo: 3 }} |
| 1 | round | |
| {{ 4.6 \ | round }} | 5 |
| ceil / floor | {{ 4.2 \ | ceil }} |
| 5 | abs | |
| {{ -5 \ | abs }} | 5 |
Array Filters
These are critical when working with product variants, images, and collection data.
{% assign sale_products = collection.products | where: 'compare_at_price_min', '>', 0 %}
{% assign sorted = collection.products | sort: 'price' %}
{% assign titles = collection.products | map: 'title' %}
{% assign unique_vendors = collection.products | map: 'vendor' | uniq %}| Filter | Purpose |
|---|---|
| where | Filter items by property value |
| sort / sort_natural | Sort by property (case-sensitive / case-insensitive) |
| map | Extract a single property from each item |
| first / last | Return first or last element |
| size | Return array length |
| join | Combine elements into a string |
| concat | Merge two arrays |
| uniq | Remove duplicates |
| compact | Remove nil values |
| reverse | Reverse array order |
Money Filters
Never format prices manually. Shopify's money filters respect your store's currency settings.
{{ product.price | money }} <!-- $25.00 -->
{{ product.price | money_with_currency }} <!-- $25.00 USD -->
{{ product.price | money_without_trailing_zeros }} <!-- $25 -->
{{ product.price | money_without_currency }} <!-- 25.00 -->URL and Media Filters
The modern image_url and image_tag filters replaced the older img_url and img_tag. Use the new versions for better performance and responsive image support.
<!-- Modern responsive image (recommended) -->
{{ product.featured_image | image_url: width: 800 | image_tag:
loading: 'lazy',
widths: '200,400,600,800',
class: 'product-image' }}
<!-- Asset URLs -->
{{ 'style.css' | asset_url | stylesheet_tag }}
{{ 'app.js' | asset_url | script_tag }}
{{ 'logo.png' | file_url }}Section Schemas and Blocks

Section schemas are the bridge between your Liquid code and the theme editor. They define what merchants can customize without touching code.
Basic Section Schema
{% schema %}
{
"name": "Featured Product",
"tag": "section",
"class": "featured-product",
"limit": 1,
"settings": [
{
"type": "product",
"id": "product",
"label": "Product"
},
{
"type": "checkbox",
"id": "show_vendor",
"label": "Show vendor",
"default": true
},
{
"type": "color",
"id": "background_color",
"label": "Background color",
"default": "#ffffff"
}
]
}
{% endschema %}Access settings in the template with {{ section.settings.show_vendor }}.
Blocks
Blocks let merchants add, remove, and reorder content within a section. Always include {{ block.shopify_attributes }} on the top-level HTML element for theme editor compatibility.
{% schema %}
{
"name": "Slideshow",
"max_blocks": 6,
"blocks": [
{
"type": "slide",
"name": "Slide",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Image"
},
{
"type": "text",
"id": "heading",
"label": "Heading"
}
]
}
],
"presets": [
{
"name": "Slideshow",
"blocks": [
{ "type": "slide" },
{ "type": "slide" }
]
}
]
}
{% endschema %}
{% for block in section.blocks %}
<div class="slide" {{ block.shopify_attributes }}>
{{ block.settings.image | image_url: width: 1200 | image_tag }}
<h2>{{ block.settings.heading }}</h2>
</div>
{% endfor %}Template Restrictions
Control where a section appears using enabled_on and disabled_on:
{
"enabled_on": {
"templates": ["product", "collection"],
"groups": ["body"]
}
}For a deeper dive into building editable sections without apps, see our guide on creating custom Shopify sections natively.
Metafields and Metaobjects in Liquid

Metafields extend your store's data model. They're how you add custom fields like fabric type, warranty info, or sizing charts to products, collections, and more. If you're new to metafields, our complete metafields guide walks through the setup process.
Accessing Product Metafields
<!-- Single-line text metafield -->
{{ product.metafields.custom.fabric_type.value }}
<!-- Rich text metafield -->
{{ product.metafields.custom.care_instructions | metafield_tag }}
<!-- List metafield (iterate over values) -->
{% for feature in product.metafields.custom.features.value %}
<li>{{ feature }}</li>
{% endfor %}
<!-- File reference (image) -->
{{ product.metafields.custom.size_chart.value | image_url: width: 600 | image_tag }}Accessing Metaobjects
Metaobjects are reusable structured content entries. Access them by type and handle.
<!-- Single metaobject by handle -->
{% assign testimonial = metaobjects.testimonials.featured %}
<blockquote>
<p>{{ testimonial.quote.value }}</p>
<cite>{{ testimonial.author.value }}</cite>
</blockquote>
<!-- Loop through all metaobjects of a type -->
{% for item in metaobjects.testimonials.values %}
<div class="testimonial">
<p>{{ item.quote.value }}</p>
</div>
{% endfor %}Important limitations:
- Maximum 20 unique metaobject handles per page
- Only metaobjects with
activestatus are accessible (drafts return nil) - Metaobject collections support pagination up to 250 items per page
Theme Tags: render, layout, and content_for
Theme tags control how templates, sections, and snippets fit together. Getting these right is critical for theme architecture.
render (Replaces include)
The render tag is the only way to include snippets in modern Shopify themes. The deprecated include tag shared variable scope across snippets, which caused naming conflicts and performance issues. render scopes variables locally.
<!-- Pass variables explicitly -->
{% render 'product-card', product: product, show_vendor: true %}
<!-- Render for each item in a collection -->
{% render 'product-card' for collection.products as product %}Best practices for `render`:
- Always pass variables explicitly --
rendercannot access parent scope - Use
forsyntax when rendering a snippet for each item in an array - Use
withsyntax to pass a single object:{% render 'icon' with 'cart' as icon_name %}
layout
Controls which layout file wraps the current template. Most templates use the default theme.liquid.
<!-- Use an alternate layout -->
{% layout 'checkout' %}
<!-- No layout (bare HTML output) -->
{% layout none %}content_for_header (Required)
Every theme.liquid file must include {{ content_for_header }} in the <head> tag. This outputs Shopify's required scripts for analytics, checkout, and app integrations.
<head>
<meta charset="utf-8">
{{ content_for_header }}
{{ 'base.css' | asset_url | stylesheet_tag }}
</head>Performance Patterns for Liquid
Slow Liquid rendering is the number one cause of poor TTFB in Shopify stores. According to Shopify's performance team, nested loop data processing can add 2-3 seconds to page render time. Here's how to avoid the most common bottlenecks.
Avoid N+1 Query Patterns
The worst performance anti-pattern is fetching data inside a loop. Assign the data once before the loop.
<!-- BAD: N+1 pattern -->
{% for item in cart.items %}
{% assign product = all_products[item.product.handle] %}
{{ product.metafields.custom.warranty.value }}
{% endfor %}
<!-- GOOD: Data is already on the item -->
{% for item in cart.items %}
{{ item.product.metafields.custom.warranty.value }}
{% endfor %}Cache Repeated Calculations
If you reference the same computed value multiple times, store it with assign or capture.
<!-- BAD: Recalculating each time -->
{% if collection.products | where: 'available' | size > 0 %}
<p>{{ collection.products | where: 'available' | size }} items in stock</p>
{% endif %}
<!-- GOOD: Calculate once -->
{% assign available_products = collection.products | where: 'available' %}
{% assign available_count = available_products | size %}
{% if available_count > 0 %}
<p>{{ available_count }} items in stock</p>
{% endif %}LCP Image Optimization
Never lazy-load your above-the-fold hero image. SpeedBoostr's Liquid optimization guide confirms that lazy-loading the LCP image causes roughly 1 second slower median LCP.
<!-- Hero image: eager load with high priority -->
{{ section.settings.hero_image | image_url: width: 1200 | image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '400,600,800,1200' }}
<!-- Below-fold images: lazy load -->
{{ product.featured_image | image_url: width: 600 | image_tag:
loading: 'lazy',
widths: '200,400,600' }}Performance Quick Wins
| Do This | Not This | Why |
|---|---|---|
| Use render | Use include | Scoped variables, better caching |
| assign before loops | Compute inside loops | Prevents N+1 queries |
| image_url + image_tag | img_url + img_tag | Modern responsive images |
| paginate by 12 | Load all products | Respects 50-iteration limit |
| Eager-load LCP image | Lazy-load everything | ~1s faster LCP |
| Use Theme Inspector | Guess at bottlenecks | Chrome extension pinpoints slow Liquid |
Common Mistakes and What to Avoid
After years of reviewing Shopify themes, these are the patterns that cause the most bugs, performance issues, and merchant frustration. Refer to our Liquid coding best practices guide for deeper explanations.
Mistake 1: Using include Instead of render
The include tag is deprecated. It leaks variables across snippets, causes hard-to-trace bugs, and degrades performance. Always use render and pass variables explicitly.
Mistake 2: Hardcoding Strings Instead of Using Translations
<!-- BAD -->
<button>Add to Cart</button>
<!-- GOOD -->
<button>{{ 'products.product.add_to_cart' | t }}</button>Hardcoded strings break internationalization and make it impossible for merchants to customize text from the theme editor's language settings.
Mistake 3: Forgetting block.shopify_attributes
Every block's top-level HTML element must include {{ block.shopify_attributes }}. Without it, the theme editor can't select, move, or configure individual blocks.
Mistake 4: Not Using Whitespace Control
Liquid outputs whitespace for every tag, which bloats HTML. Use the hyphen syntax to strip whitespace.
<!-- Without whitespace control (adds blank lines) -->
{% assign name = 'Shopify' %}
{{ name }}
<!-- With whitespace control (clean output) -->
{%- assign name = 'Shopify' -%}
{{ name }}Mistake 5: Ignoring the 50-Iteration Limit
Shopify's for loop caps at 50 iterations. If your collection has more than 50 products, the loop silently stops. Always wrap large collections in {% paginate %}.
Mistake 6: Lazy-Loading Above-the-Fold Images
This single mistake costs more performance than almost anything else. Set loading: 'eager' and fetchpriority: 'high' on your hero and LCP images.
Developer Tooling and Workflow Tips
The right tools make Liquid development dramatically faster. Shopify's Winter '26 Editions introduced several upgrades worth knowing about.
Theme Check (Linter)
Theme Check is Shopify's official linter for Liquid and JSON. It catches syntax errors, deprecated tags, accessibility issues, and performance anti-patterns. Install it as a VS Code extension or run it via the Shopify CLI.
shopify theme checkShopify CLI for Theme Development
# Start local development server with hot reload
shopify theme dev --store your-store.myshopify.com
# Push theme to store
shopify theme push
# Pull current live theme
shopify theme pull
# Run against multiple environments (new in Winter '26)
shopify theme dev --environments development,stagingTheme Inspector Chrome Extension
The Theme Inspector overlays Liquid rendering times directly on your storefront. It shows exactly which sections, snippets, and loops are slow, so you can focus optimization where it matters most.
Dev MCP (Winter '26)
Shopify's Dev MCP lets AI agents generate validated Liquid code, run GraphQL operations, and scaffold apps. If you're using AI-powered development tools, this integration dramatically reduces context-switching.
Quick Reference: Common Liquid Patterns
These copy-paste patterns cover the scenarios you'll encounter most often.
Conditional Add-to-Cart Button
{%- if product.available -%}
<button type="submit" class="btn btn--primary">
{{ 'products.product.add_to_cart' | t }} - {{ product.price | money }}
</button>
{%- else -%}
<button type="button" class="btn btn--disabled" disabled>
{{ 'products.product.sold_out' | t }}
</button>
{%- endif -%}Responsive Product Image with Srcset
{%- assign image = product.featured_image -%}
{{ image | image_url: width: 1200 | image_tag:
loading: 'lazy',
widths: '200,400,600,800,1000,1200',
sizes: '(min-width: 1024px) 50vw, 100vw',
alt: image.alt | escape }}Sale Price with Compare-at Pricing
{%- if product.compare_at_price > product.price -%}
<s class="price--compare">{{ product.compare_at_price | money }}</s>
<span class="price--sale">{{ product.price | money }}</span>
{%- assign savings = product.compare_at_price | minus: product.price -%}
<span class="price--savings">Save {{ savings | money }}</span>
{%- else -%}
<span class="price">{{ product.price | money }}</span>
{%- endif -%}Customer-Tag-Based Content
{%- if customer -%}
{%- if customer.tags contains 'wholesale' -%}
<p>Wholesale pricing applied</p>
{%- elsif customer.tags contains 'vip' -%}
<p>VIP member: free shipping on all orders</p>
{%- endif -%}
{%- else -%}
<p><a href="/account/login">Log in</a> for member pricing</p>
{%- endif -%}Collection Filtering with Pagination
{%- paginate collection.products by 12 -%}
<div class="product-grid">
{%- for product in collection.products -%}
{% render 'product-card', product: product %}
{%- endfor -%}
</div>
{%- if paginate.pages > 1 -%}
<nav class="pagination">
{{ paginate | default_pagination: next: '→', previous: '←' }}
</nav>
{%- endif -%}
{%- endpaginate -%}AJAX Cart Section Rendering
Use the Section Rendering API to update cart sections without a full page reload.
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: variantId,
quantity: 1,
sections: 'cart-drawer,cart-icon-bubble'
})
})
.then(response => response.json())
.then(data => {
document.getElementById('cart-drawer').innerHTML =
data.sections['cart-drawer'];
});FAQ: Shopify Liquid Development
What's the difference between `render` and `include`? render scopes variables locally, preventing naming conflicts. include (deprecated) shared parent scope, which caused hard-to-debug issues. Always use render in Online Store 2.0 themes.
How do I access product metafields in Liquid? Use the syntax {{ product.metafields.namespace.key.value }}. For example: {{ product.metafields.custom.fabric_type.value }}. The .value accessor is required.
Why does my `for` loop stop at 50 items? Shopify enforces a 50-iteration limit per for loop. Wrap your collection in {% paginate collection.products by 50 %} to access all items in paginated chunks.
Can I use JavaScript inside Liquid files? Yes. Liquid files can contain HTML, CSS, and JavaScript. However, Liquid tags are rendered server-side before JavaScript executes. Use the Section Rendering API or Shopify's AJAX APIs to update content dynamically after page load.
How do I debug slow Liquid rendering? Install the Shopify Theme Inspector Chrome extension. It overlays rendering times on your storefront, showing exactly which Liquid files and loops are causing bottlenecks.
Your Liquid Development Workflow Starts Here
This Shopify Liquid cheat sheet for developers covers the syntax, patterns, and performance strategies that matter in day-to-day theme development. Bookmark it, reference it while you build, and pair it with the official Shopify Liquid reference for the complete object and filter documentation.
The fastest path to Liquid mastery isn't memorizing every filter. It's building real themes, hitting real problems, and knowing exactly where to look when you get stuck.
Ready to level up your Shopify development skills? Join the Talk Shop community where developers share Liquid tips, review each other's theme code, and stay ahead of Shopify platform updates.
What Liquid pattern or filter do you find yourself looking up most often? Drop it in the comments and we'll add it to this cheat sheet.

About Talk Shop
The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.
Related Insights
The ecommerce newsletter that's actually useful.
Daily trends, teardowns, and tactics from the top 1% of ecommerce brands. Delivered every morning.
