{{ section.settings.heading | escape }}
{{ product.title | default: 'Product' | escape }}Liquid Cheatsheet
Quick reference guide for Shopify Liquid templating. Search, copy, and learn.
A searchable practice surface, not a Shopify storefront runtime
Use the snippets and mock-data playground to learn Liquid syntax and inspect common output. The browser preview implements a useful subset for learning; verify store objects, Shopify-only filters, forms, and theme-editor behavior in an actual development theme.
89 snippets · press / to search, j/k to navigate
Variables
4{{ product.title }}
{{ product.price | money }}
{{ customer.first_name | default: "Guest" }}{% assign sale_price = product.price | times: 0.8 | round %}
Sale price: {{ sale_price | money }}
{% assign greeting = "Hello, " | append: customer.first_name | default: "Guest" %}
{{ greeting }}{% capture product_card %}
<div class="card">
<h3>{{ product.title }}</h3>
<span>{{ product.price | money }}</span>{% increment counter %}
{% increment counter %}
{% increment counter %}
ID example: section-Control Flow
5{% if product.available %}
<button>Add to Cart</button>
{% elsif product.compare_at_price > product.price %}
<button disabled>Coming Soon</button>{% if cart.item_count == 0 %}
Your cart is empty
{% elsif cart.item_count == 1 %}
You have 1 item{% unless product.available == false %}
<button>Buy Now — {{ product.price | money }}</button>
{% endunless %}
{% unless customer %}{% case product.type %}
{% when "Electronics" %}
<span>⚡ Tech Product</span>
{% when "Clothing" %}{% if product.price > 5000 and product.available %}
Premium & In Stock
{% endif %}
{% if product.tags contains "sale" %}Loops
8{% for product in collection.products %}
<div class="product">{{ product.title }} — {{ product.price | money }}</div>
{% endfor %}
{% for tag in product.tags %}First 2 products:
{% for product in collection.products limit: 2 %}
{{ product.title }}
{% endfor %}{% for item in collection.products %}
{% if forloop.first %}
<ul>
{% endif %}{% assign empty_array = "" | split: "," %}
{% for item in empty_array %}
{{ item }}
{% else %}{% for product in collection.products %}
<div class="col
{% cycle 'col-odd', 'col-even' %}
">Show only available products, stop after finding sold-out:
{% for product in collection.products %}
{% unless product.available %}
{% break %}{%- assign filtered = collection.products -%}
{% comment %}
Apply tag filter from URL: /collections/all/sale
{% endcomment %}<table>
{% tablerow product in collection.products cols: 2 %}
{{ product.title }}<br>
<strong>{{ product.price | money }}</strong>Filters
16{{ product.price | money }}
{{ product.price | money_with_currency }}
{{ product.price | money_without_currency }}
{{ product.compare_at_price | money_without_trailing_zeros }}{% assign img = product.featured_image %}
Simple image:
<img src="{{ img | image_url: width: 800 }}"
alt="{{ img.alt | escape }}"{{ "hello world" | upcase }}
{{ "HELLO WORLD" | downcase }}
{{ "hello world" | capitalize }}
{{ " trim me " | strip }}{{ "My Product Title!" | handleize }}
Reverse handle to readable:
{{ product.handle | replace: "-", " " | capitalize }}
Build a collection URL:{{ 16 | divided_by: 4 }}
{{ 3 | times: 4 }}
{{ 10 | plus: 5 }}
{{ 10 | minus: 3 }}{{ article.published_at | date: "%B %d, %Y" }}
{{ article.published_at | date: "%Y-%m-%d" }}
{{ article.published_at | date: "%b %-d" }}
{{ "now" | date: "%H:%M" }}Filter by property:
{% assign available = collection.products | where: "available", true %}
{{ available.size }} available products
Extract one property:{% assign arr = "a,b,c,d,e" | split: "," %}
First: {{ arr | first }}
Last: {{ arr | last }}
Size: {{ arr | size }}{% assign vendor = product.vendor %}
{% assign first_match = collection.products | find: "vendor", vendor %}
{% assign match_index = collection.products | find_index: "vendor", vendor %}
{% assign has_match = collection.products | has: "vendor", vendor %}{{ product.vendor | default: "Unknown Vendor" }}
{{ product.metafields.custom.subtitle | default: product.title }}
{{ customer.first_name | default: "Guest" }}
Empty string also triggers default:{% assign html = "<p>Hello <strong>World</strong>
</p>" %}
{{ html | strip_html }}
{{ html | escape }}{% assign primary = settings.colors_primary %}
Convert formats:
{{ primary | color_to_rgb }}
{{ primary | color_to_hsl }}{{ product.tags | size }} tags
{{ product.title | size }} characters
{{ collection.products | size }} products
Conditional on size:Locales live in locales/en.default.json:
{ "products": { "add_to_cart": "Add to cart",
"sold_out": "Sold out",
"items_count": "{{ count }} items" } }{%- if cart.currency == shop.currency -%}
{{ product.price | money }}
{%- else -%}
{{ product.price | money }}{% comment %}
Translation key references locales/en.default.json:
{
"products": {Objects
17<h1>{{ product.title }}</h1>
<p>by {{ product.vendor | link_to_vendor }} · {{ product.type | link_to_type }}</p>
<p>{{ product.description | truncate: 120 }}</p>
<p>{% for option in product.options_with_values %}
<label>{{ option.name }}</label>
<select name="option{{ option.position }}">
{% for value in option.values %}{% assign variant = product.selected_or_first_available_variant %}
{% if product.available %}
{% if variant.inventory_management == "shopify" %}
{% if variant.inventory_quantity > 10 %}Single value:
{{ product.metafields.custom.subtitle.value }}
Rating metafield:
{% assign rating = product.metafields.reviews.rating %}<h1>{{ collection.title }}</h1>
<p>{{ collection.description }}</p>
<p>{{ collection.products_count }} products</p>
<div class="product-grid"><span>{{ cart.item_count }} {{ cart.item_count | pluralize: "item", "items" }}</span>
<span>{{ cart.total_price | money }}</span>
{% for item in cart.items %}
<div class="cart-item">{% if shop.customer_accounts_enabled %}
<shopify-account menu="{{ section.settings.customer_account_menu }}">
</shopify-account>
{% endif %}<p>{{ shop.name }}</p>
<p>Currency: {{ shop.currency }}</p>
<p>Domain: {{ shop.domain }}</p>
<a href="mailto:{{ shop.email }}">Contact us</a><title>{{ page_title }}</title>
{% if page_description %}
<meta name="description"
content="{{ page_description | escape }}"><nav aria-label="Main navigation">
{% for link in section.settings.menu.links %}
{% if link.links.size > 0 %}
<details class="dropdown">Page type: {{ request.page_type }}
Host: {{ request.host }}
Path: {{ request.path }}
Locale: {{ request.locale.iso_code }}<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",{% if cart.item_count > 0 %}
{%- assign cart_token = cart.token -%}
<a href="{{ shop.url }}/cart/{{ cart_token }}?recover=1"
class="recovery-link">{%- assign limit = section.settings.limit | default: 4 -%}
{%- assign intent = section.settings.intent | default: 'related' -%}
<div data-product-recommendations
data-product-id="{{ product.id }}"{%- assign designer = product.metafields.custom.designer.value -%}
{% if designer %}
<div class="designer-card">
<h3>{{ designer.name.value }}</h3>{% for group in product.selling_plan_groups %}
<fieldset class="selling-plan-group">
<legend>{{ group.name }}</legend>
{% for plan in group.selling_plans %}<h3>Shipping to</h3>
{{ customer.default_address | format_address }}
Or build it manually:
{%- assign addr = customer.default_address -%}Schema
7<h2 style="color: {{ section.settings.text_color }}">
{{ section.settings.heading }}
</h2>
{% if section.settings.show_button %}{% for block in section.blocks %}
<div {{ block.shopify_attributes }}>
{% case block.type %}
{% when "heading" %}<section class="hero"
style="color: {{ section.settings.text_color }}">
{% for block in section.blocks %}
{% case block.type %}String types — output directly:
{{ section.settings.title }}
{{ section.settings.content }}
Number / range:<style>
:root {
--color-primary: {{ settings.colors_primary }};
--color-secondary: {{ settings.colors_secondary }};<div class="section" {{ section.shopify_attributes }}>
{% content_for 'blocks' %}
</div>{% content_for 'block',
type: 'slideshow-controls',
id: 'hero-controls',
color: section.settings.control_colorTags
11{% form 'product', product %}
<input type="hidden" name="id"
value="{{ product.selected_or_first_available_variant.id }}">
{% if product.variants.size > 1 %}{% paginate collection.products by 3 %}
{% for product in collection.products %}
<div>{{ product.title }} — {{ product.price | money }}</div>
{% endfor %}{% render 'product-card', product: product %}
{% render 'icon', name: 'cart', size: 24, color: '#000000' %}
{% render 'product-card' for collection.products as product %}
{% render 'product-card' for collection.products as product,{% liquid
assign greeting = "Hello"
assign name = customer.first_name | default: "Guest"
assign full = greeting | append: ", " | append: name | append: "!"{% comment %}
Standard comment — renders nothing.
Use for documentation or disabling code.
{% endcomment %}<form action="{{ routes.search_url }}" method="get" role="search">
<input type="search"
name="q"
placeholder="Search products..."{% comment %}
Place in theme.liquid <head>
{% endcomment %}
<!-- DNS prefetch + preconnect for CDN -->{% liquid
assign show_badge = false
if product.available and settings.show_product_reviews
assign show_badge = true{% block 'container', tag: 'section', class: 'featured-products' %}
<h2>{{ collection.title }}</h2>
{% block 'product-grid', block.settings.collection: collection %}
{% endblock %}{% partial 'product-grid' %}
{% for product in collection.products %}
{% render 'product-card', product: product %}
{% endfor %}Forms
6{% form 'contact' %}
{{ form.errors | default_errors }}
<label for="contact-name">Name</label>
<input type="text" id="contact-name" name="contact[name]"{%- assign color_option = product.options_with_values | where: 'name', 'Color' | first -%}
{% if color_option %}
<p class="swatch-current">Color: <strong>{{ color_option.selected_value }}</strong>
</p><div class="quantity-input" data-quantity>
<button type="button"
name="minus"
aria-label="Decrease quantity"<form action="{{ routes.cart_add_url }}" method="post" data-cart-form>
<input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
<input type="number" name="quantity" value="1" min="1">
<button type="submit"{% form 'localization' %}
<label for="country">Country/region</label>
<select name="country_code" id="country">
{% for country in localization.available_countries %}Media
3{% for media in product.media %}
{% case media.media_type %}
{% when "video" %}
<div class="video-wrapper">{% for media in product.media %}
{% if media.media_type == "model" %}
<div class="model-wrapper" style="aspect-ratio: 1;">
{{ media | model_viewer_tag:{% assign img = product.featured_image %}
{% assign widths = "200,400,600,800,1200,1600" %}
<img src="{{ img | image_url: width: 600 }}"
srcset="Pagination
2{% paginate collection.products by 6 %}
<div class="product-grid">
{% for product in collection.products %}
<div class="product-card">{% paginate collection.products by 6 %}
<p>
Page {{ paginate.current_page }} of {{ paginate.pages }}
({{ paginate.items }} total products)Sections
7In layout/theme.liquid:
{% section 'header' %}
{{ content_for_layout }}
{% section 'footer' %}{% schema %}
{
"name": "Featured Collection",
"tag": "section",<div class="lazy-section" data-section-id="{{ section.id }}">
<div class="lazy-placeholder" style="min-height: 400px; background: #f3f4f6;">
<noscript>
{% render 'section-content', section: section %}{%- liquid
assign bg_color = section.settings.bg_color | default: '#ffffff'
assign text_color = section.settings.text_color | default: '#000000'
assign padding = section.settings.padding | default: 64{% comment %}
Section rendering API: fetch updated section HTML
Useful for cart drawers, filters, AJAX pagination
{% endcomment %}<section class="dynamic-section">
{% if section.settings.heading != blank %}
<h2>{{ section.settings.heading }}</h2>
{% endif %}In layout/theme.liquid:
{% sections 'header-group' %}
{{ content_for_layout }}
{% sections 'footer-group' %}Snippets
3Basic render:
{% render 'product-card', product: product %}
With multiple parameters:
{% render 'icon', name: 'cart', size: 24, color: '#6366f1' %}{% doc %}
Renders a product card.
@param {object} product - Product to display.
@param {boolean} [show_vendor] - Show the product vendor.<button class="quick-add" type="button">
{{ 'products.product.quick_add' | t }}
</button>
{% stylesheet %}Mock Shopify Data
DefaultCore Liquid patterns to understand
Copying a snippet is only the first step. Read the object scope, output context, fallback behavior, and empty state before adapting it.
Filter and safe text output
Condition and loop
{% if section.blocks.size > 0 %}
{% for block in section.blocks %}
<article {{ block.shopify_attributes }}>
{{ block.settings.title | escape }}
</article>
{% endfor %}
{% endif %}Responsive Shopify image
{{ section.settings.image
| image_url: width: 1600
| image_tag:
widths: '480, 768, 1200, 1600',
sizes: '(min-width: 990px) 50vw, 100vw',
loading: 'lazy',
alt: section.settings.image.alt
}}Escaping by output context
{{ section.settings.label | escape }}
<script type="application/json">
{{ product | json }}
</script>Schema setting example
{
"type": "image_picker",
"id": "image",
"label": "Image"
}A setting declaration only creates the editor control. Your Liquid must still read section.settings.image, handle blank state, choose responsive widths, and decide whether the image is informative or decorative.
Common mistakes
- Rendering a merchant string without
escapein a plain-text HTML context. - Wrapping a
richtextvalue inp, which can create nested paragraphs. - Using
img_urlinstead of currentimage_urlandimage_tagpatterns. - Forgetting
block.shopify_attributesin a section-block wrapper. - Assuming
product,collection, orarticleexists on every template. - Using
jsonas HTML escaping; it serializes a JavaScript/JSON value for a different output context.
For platform definitions, use Shopify's official Liquid reference, settings guide, and blocks guide. For a complete implementation, continue to responsive Shopify images or Liquid escaping and safe output.