Shopify Liquid: A Practical Guide for Theme Developers
Logo
Shopify Liquid: A Practical Guide for Theme Developers

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 pick srcset via img_tag or custom markup.
  • Lazy load below-the-fold media (loading="lazy").
  • Limit for loops and nested render 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”

Related Posts

Ready To Transform Your Digital Presence

Let's embark on a journey of innovation and creativity together.