Skip to main content

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.

Preview
Ready
Preview uses LiquidJS (client-side) — Shopify-specific filters, objects, and tags may not render accurately.
Level:

89 snippets

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 &amp; 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 }}
{{ 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_color

Tags

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: "!"
Used in layout/theme.liquid ONLY.
content_for_header injects Shopify scripts and app injections.
content_for_layout renders the current template.
<!doctype html>
{% comment %}
Standard comment — renders nothing.
  Use for documentation or disabling code.
{% endcomment %}
{% 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]"
{% form 'customer', customer, id: 'newsletter-form' %}
  <input type="hidden" name="contact[tags]" value="newsletter">
  <label for="newsletter-email" class="sr-only">Email</label>
  <input type="email" id="newsletter-email"
{%- 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

7
In 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

3
Basic 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

Default
product
collection
cart
customer
shop
settings
section
article
blog
page
request
routes
linklists
orders
gift_cards
discount
search
recommendations
localization

Core 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

{{ section.settings.heading | escape }}
{{ product.title | default: 'Product' | escape }}

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 escape in a plain-text HTML context.
  • Wrapping a richtext value in p, which can create nested paragraphs.
  • Using img_url instead of current image_url and image_tag patterns.
  • Forgetting block.shopify_attributes in a section-block wrapper.
  • Assuming product, collection, or article exists on every template.
  • Using json as 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.