
Shopify Liquid: A Practical Guide for Theme Developers
What is Liquid (and why should you care)?
Liquid is Shopify’s templating language. It glues your storefront data (products, collections, cart, metafields) to your theme UI (sections, snippets, and templates). Liquid runs on Shopify’s servers, so it’s fast, secure, and SEO-friendly—no custom backend required.
At a glance:
- Objects (data sources):
product
,collection
,cart
,blog
,linklists
(a.k.a. navigation),shop
, and more. - Tags (logic/control):
{% if %}
,{% for %}
,{% section %}
,{% render %}
. - Filters (format/transform):
| money
,| image_url
,| times:
,| default:
.
Core building blocks (with examples)
1) Output and variables
{% assign price = product.price | money %}
<p>{{ product.title }} – <strong>{{ price }}</strong></p>
2) Conditionals
{% if product.available %}
<button>Add to cart</button>
{% else %}
<span>Sold out</span>
{% endif %}
3) Loops
<ul>
{% for image in product.images limit: 4 %}
<li><img src="{{ image | image_url: width: 800 }}" alt="{{ image.alt | escape }}"></li>
{% endfor %}
</ul>
4) Filters you’ll use daily
money
,money_with_currency
escape
,json
,truncate
,strip_html
image_url
,img_tag
handleize
,downcase
,capitalize
default
,compact
,uniq
Theme structure (OS 2.0 way)
A typical theme:
/layout/theme.liquid
/templates/*.json
/sections/*.liquid
/snippets/*.liquid
/assets/* (CSS, JS, images)
/config/*.json (settings_schema.json + presets)
/locales/*.json
- Layouts wrap every page (
theme.liquid
). - Templates (JSON) map which sections render for each page type (e.g.,
product.json
,collection.json
). - Sections are the modular, draggable building blocks.
- Snippets are partials for reuse (price badges, media, SKU, etc.).
- Locales power translations.
Sections, blocks, and settings
A minimal section
{% schema %}
{
"name": "Hero banner",
"settings": [
{ "type": "image_picker", "id": "image", "label": "Background image" },
{ "type": "text", "id": "heading", "label": "Heading", "default": "New arrivals" }
],
"blocks": [
{ "type": "button", "name": "CTA", "settings": [{ "type": "url", "id": "link", "label": "Link" }, { "type": "text", "id": "label", "label": "Label", "default": "Shop now" }] }
],
"presets": [{ "name": "Hero banner" }]
}
{% endschema %}
<section class="hero">
{% if section.settings.image %}
<img src="{{ section.settings.image | image_url: width: 2400 }}" alt="">
{% endif %}
<h1>{{ section.settings.heading }}</h1>
{% for block in section.blocks %}
{% if block.type == 'button' %}
<a href="{{ block.settings.link }}" class="btn">{{ block.settings.label }}</a>
{% endif %}
{% endfor %}
</section>
- Schema defines UI controls inside the theme editor.
- Blocks add repeatable sub-components (CTAs, features, FAQs).
- Presets make the section appear in the Add section panel.
Rendering snippets and components
- Prefer
{% render 'snippet', arg: value %}
over{% include %}
(render has isolated scope).
{% render 'price', product: product, show_compare: true %}
Example snippets/price.liquid
:
{% assign price = product.price | money %}
{% if show_compare and product.compare_at_price > product.price %}
<span class="price price--sale">{{ price }}</span>
<s class="price price--compare">{{ product.compare_at_price | money }}</s>
{% else %}
<span class="price">{{ price }}</span>
{% endif %}
Metafields: custom data, clean markup
Use metafields to avoid hard-coding content.
Product highlights (list of text):
{% if product.metafields.custom.highlights %}
<ul class="highlights">
{% for bullet in product.metafields.custom.highlights.value %}
<li>{{ bullet }}</li>
{% endfor %}
</ul>
{% endif %}
Media with dimensions (file reference):
{% assign spec_pdf = product.metafields.documents.spec_sheet.value %}
{% if spec_pdf %}
<a href="{{ spec_pdf.url }}" download>Download spec sheet</a>
{% endif %}
Performance and SEO quick wins
- Use
image_url
with explicit widths; let the browser picksrcset
viaimg_tag
or custom markup. - Lazy load below-the-fold media (
loading="lazy"
). - Limit
for
loops and nestedrender
calls; avoid heavy operations inside loops. - Output structured data (JSON-LD) for products/collections.
- Keep CSS/JS lean; ship only what the template needs.
Product JSON-LD (minimal):
<script type="application/ld+json">
{
"@context":"https://schema.org/",
"@type":"Product",
"name": {{ product.title | json }},
"image": {{ product.images | map: 'src' | json }},
"description": {{ product.description | strip_html | truncate: 300 | json }},
"sku": {{ product.selected_or_first_available_variant.sku | json }},
"offers": {
"@type":"Offer",
"priceCurrency": {{ shop.currency | json }},
"price": {{ product.price | divided_by: 100.0 | json }},
"availability": "{{ product.available | default: false | replace: 'true','https://schema.org/InStock' | replace: 'false','https://schema.org/OutOfStock' }}"
}
}
</script>
Cart, checkout, and what Liquid can’t do
- Cart is fully customizable in theme.
- Checkout customization is limited on standard plans (brand colors, logo). Deep checkout edits require Shopify Plus (Checkout Extensibility, UI extensions).
- Use Shopify Functions/Apps for logic beyond Liquid (discounts, shipping rates, validation).
App blocks and dynamic sources
- Many apps ship app blocks you can add to templates—no code copy/paste.
- Bind section settings to dynamic sources (metafields, translations, global settings) for reusability.
Debugging tricks
- Temporarily print values:
<pre>{{ product | json }}</pre>
- Guard against
nil
:
{{ product.vendor | default: "Unknown brand" }}
- Use the Theme Inspector for Liquid (profiling) and Preview with different handles.
Common patterns you’ll reuse
- Price & badges: compare-at, percentage off, “Only X left” (from
variant.inventory_quantity
). - Swatches: variants grouped by option name (“Color”), each swatch selects a variant.
- Pagination: use
paginate
for blogs/collections. - Breadcrumbs: collection → product; fall back to default collection or all products.
Pagination example:
{% paginate collection.products by 24 %}
{% for product in collection.products %}
{% render 'card-product', product: product %}
{% endfor %}
<nav class="pagination">
{% if paginate.previous %}<a href="{{ paginate.previous.url }}">Previous</a>{% endif %}
<span>{{ paginate.current_page }} / {{ paginate.pages }}</span>
{% if paginate.next %}<a href="{{ paginate.next.url }}">Next</a>{% endif %}
</nav>
{% endpaginate %}
A 60-minute product page plan
Minutes 0–10: Duplicate theme, create templates/product.json
preset.
10–30: Add sections: media gallery, title/price, buy box, tabs/accordion, related products.
30–45: Wire metafields (highlights, care, size guide), render badges, add JSON-LD.
45–60: Test variants, accessibility (labels, focus), image sizes, mobile layout.
FAQs
Do I need a JavaScript framework?
Not for core storefront. Liquid + small JS (Alpine/vanilla) is enough for most themes. Add frameworks only when component complexity truly requires them.
Can I reuse sections across templates?
Yes. With template JSON, sections are portable. Keep content in metafields/dynamic sources to avoid duplication.
What’s the fastest way to learn Liquid?
Start by editing an existing section/snippet, print variables with | json
, and build one reusable snippet (price, badges, or swatches).
Suggested CTAs
- “Explore our Shopify theme services”
- “Read the Liquid reference cheatsheet”
- “Get a free theme performance audit”