Knowledge Hub
What is Shopify Liquid?
Liquid is a template language Shopify uses to combine static HTML with dynamic store data. It is not a full programming language—you will not build APIs in Liquid. Instead, you output product titles, loop over collections, show cart counts, and read merchant settings from section schema.
If you know HTML and basic templating, Liquid feels familiar within a day. This guide covers the core concepts; /shopify-cheat-sheet is your ongoing reference.
Liquid syntax basics
- {{ }} — output tags that render values
- {% %} — logic tags for if, for, assign, render
- Pipe filters transform output: {{ price | money }}
- Comments: {% comment %} ... {% endcomment %}
Variables and assign
Variables store temporary values. Use assign to name a collection, build a class string, or cache a computed value inside a template.
assign example
{% assign sale_collection = collections['sale'] %}
{% assign heading_class = 'heading heading--' | append: section.settings.size %}Objects and dot notation
Shopify exposes objects—product, collection, cart, shop, section, block—with properties you access via dot notation. On a product template, product.title returns the current product name. Inside sections, section.settings.heading returns the merchant-configured heading.
- product — title, price, featured_image, variants, url
- collection — products, title, url, image
- cart — item_count, total_price, items
- section.settings — values from your section schema
- block.settings — values from a block inside a section
Loops
for tags iterate arrays. The most common beginner loops: products in a collection, items in cart, and blocks in a section.
Beginner loop
{% for product in collection.products limit: 4 %}
<p>{{ forloop.index }} — {{ product.title }}</p>
{% endfor %}forloop.index, first, and last help with zebra striping and first-item styling.
Conditions
if tags branch on availability, tags, settings, and blank checks. Use elsif for multiple branches and unless as inverted if.
if / unless
{% if section.settings.heading != blank %}
<h2>{{ section.settings.heading }}</h2>
{% endif %}
{% unless product.available %}
<span>Sold out</span>
{% endunless %}Beginner workflow: HTML to section
- Build or receive static HTML for a component.
- Paste into /converter — settings and blocks are inferred.
- Review output; learn schema structure in /shopify-schema-guide.
- Add section.liquid to theme; register on JSON template per /shopify-section-tutorial.
- Let merchants edit content in the theme editor.
Common beginner mistakes
- Hardcoding text that merchants should edit—use section.settings instead.
- Forgetting presets—sections won't appear in Add section without them.
- Global CSS that breaks other sections—scope with section.id.
- Using include instead of render for snippets.
- Invalid schema JSON—validate before deploying.
Where to go next
Read /how-shopify-sections-work for architecture, /shopify-liquid-examples for copy-ready patterns, and /dynamic-shopify-blocks-guide when you need repeatable merchant-managed rows.
How Liquid fits inside a Dawn-style theme
When you open a Shopify Online Store 2.0 theme like Dawn, Liquid is not one file—it is the glue between JSON templates, section files, snippets, and locale strings. Templates in templates/index.json declare which sections render and in what order. Each section file in sections/ is mostly Liquid markup plus a schema block at the bottom. Snippets in snippets/ hold reusable fragments—product cards, icon SVGs, price rows—that sections call with render. As a beginner, resist editing layout.liquid on day one. Most client work happens in sections and snippets because merchants can configure those in the theme editor without touching code. Liquid reads store objects (product, collection, cart) that Shopify injects based on the current URL, and it reads section.settings values that merchants set in the sidebar. Understanding that split—global objects versus merchant settings—is the mental model that makes everything else click.
Liquid data flow on a product page
Browser request: /products/handle
│
▼
┌─────────────────────┐
│ product.json │ ← JSON template lists section order
│ (templates/) │
└──────────┬──────────┘
│ type: "main-product"
▼
┌─────────────────────┐
│ main-product.liquid│ ← Liquid outputs HTML
│ + {% schema %} │ ← schema defines editor fields
└──────────┬──────────┘
│
┌────────┴────────┐
▼ ▼
product.title section.settings
(Shopify object) (merchant input)On any template, Shopify provides context objects while schema supplies merchant-editable values. Your Liquid combines both into HTML.
Output tags, whitespace, and the dash trick
Beginners often wonder why their HTML has unexpected blank lines. Liquid output tags {{ }} preserve whitespace from the template file. Logic tags {% %} can also leave gaps when they sit on their own lines. Shopify theme developers use hyphen trims to suppress whitespace: {{- product.title -}} strips surrounding space, and {%- if product.available -%} prevents empty lines from conditional blocks. This matters in inline layouts—badges beside prices, icon rows, flex children—where a stray newline becomes visible gap. You will see this pattern throughout Dawn: trimmed tags around small inline elements. Do not trim everything blindly; readable source still helps during code review. Trim where rendered HTML spacing breaks the design. When you paste static HTML into /converter, inspect whether the tool wrapped inline spans with trimmed output tags. That is a detail worth copying into hand-written sections too.
Whitespace control
{%- if product.compare_at_price > product.price -%}
<span class="badge badge--sale">Sale</span>
{%- endif -%}
<span class="price">{{ product.price | money }}</span>Hyphens on {% %} and {{ }} tags remove the newline Liquid would otherwise output between elements.
When to use assign versus capture
assign stores a simple value—a string, number, object reference, or boolean result from a comparison. capture wraps a chunk of rendered markup and saves it as a string. Beginners reach for capture when building class lists, aria labels, or deferred HTML blocks that depend on multiple conditions. A common pattern: capture a modifier string, then output it once inside a class attribute. Another: capture a metafield-driven badge only when the metafield exists, then print the capture variable once outside a noisy if block. capture is slower than assign in theory, but in section rendering the difference is negligible compared to image loading. Readability wins. If you find yourself chaining five assign statements to build one class name, switch to capture for clarity.
Objects you will touch on every client project
product and collection are the workhorses. On a collection template, collection.products is the array you loop for grids. On product templates, product.variants drives variant pickers and product.selected_or_first_available_variant is the safe default when you need a single SKU for price display. cart is global—useful in header snippets for item counts. shop holds store name, currency, and domain. request.page_type and template.name let you branch behavior: hide a promo banner on cart and checkout-adjacent pages, or load a different snippet on index versus collection. routes is easy to overlook; routes.cart_url and routes.account_url generate correct localized paths without hardcoding /cart. Hardcoded URLs break when merchants add markets or change URL structures. Beginners copy static href values from HTML mockups; experienced developers replace them with routes or url filters immediately.
- product.url — canonical product link; use with link filters for absolute URLs in meta tags
- product.featured_media — preferred over featured_image in modern themes; handles video
- collection.handle — stable slug for collection lookups via collections[handle]
- cart.item_count — integer for header badge; check > 0 before showing the dot
- section.settings — always namespace settings in schema; never assume a global variable exists
- block.settings — only valid inside a for block in section.blocks loop
Loops, limits, and forloop helpers
The for tag accepts limit, offset, and reversed. limit is your performance friend—never render forty product cards if the design shows four. offset skips leading items when you already featured the first product elsewhere. forloop.index is one-based; forloop.index0 is zero-based. forloop.first and forloop.last help with border-radius on card grids without modulo math. forloop.length tells you how many items exist after limit is applied, which is useful for empty-state messaging. When looping section.blocks, always print block.shopify_attributes on the wrapper element so the theme editor highlights the correct row when a merchant clicks it. Missing shopify_attributes is a top reason blocks feel broken in the editor even when the storefront looks fine. Nested loops—products inside a collection tab inside a section—get messy fast. Extract inner markup to snippets early.
Collection grid with forloop styling
{% for product in collection.products limit: section.settings.products_to_show %}
<div class="card{% if forloop.first %} card--featured{% endif %}"
{{ block.shopify_attributes }}>
{% render 'product-card', product: product, lazy: forloop.index > 2 %}
</div>
{% else %}
<p>No products in this collection yet.</p>
{% endfor %}The {% else %} branch on a for loop runs when the collection is empty—a beginner-friendly empty state merchants appreciate.
Conditions that prevent embarrassing storefront bugs
Blank checks matter. if section.settings.heading != blank prevents empty h2 tags that hurt accessibility and SEO. For images, test both the picker value and whether image_url returns something useful. product.available is not the same as inventory_quantity > 0 in every edge case—preorder tags and continue-selling settings complicate stock display. The contains operator works on strings and arrays: product.tags contains 'featured' is a common merchandising gate. For case-insensitive tag checks, pipe through downcase first. unless is readable for single negations; nested unless blocks become hard to audit—prefer explicit if with elsif when business rules multiply. Compare_at_price > price is the standard on-sale test, but some merchants schedule sales differently; document assumptions in your handoff notes when you gate sale badges on that comparison alone.
From HTML mockup to editable section—a realistic first project
- 01
Paste markup into /converter
Start with the hero or feature band your designer delivered. Remove script tags and third-party embeds that do not belong in a section file. The converter infers text fields, images, and repeatable rows from your DOM structure.
- 02
Read the generated schema before deploying
Open /shopify-schema-guide alongside the output. Confirm setting ids match the content they control—heading for the h2, cta_label for the button text. Rename vague ids like text_1 while you still have zero merchant data on the section.
- 03
Drop the file into sections/ and add a preset
Filename becomes the section type: sections/promo-banner.liquid → type promo-banner in JSON. Without a preset, merchants cannot add the section from the editor's Add section panel.
- 04
Register on index.json or the target template
Add a sections entry and include its key in order. Ship sensible defaults in settings so the first preview is not empty boxes. Follow /shopify-section-tutorial for JSON template anatomy.
- 05
QA in the theme editor on mobile and desktop
Toggle every checkbox, clear optional images, and add two extra blocks if the section supports blocks. Fix blank-state markup before the client sees it.
Your first section does not need every schema setting type. A text field, an image_picker, and a url setting cover most hero bands. Add range sliders for padding only after the layout is approved—early padding controls distract stakeholders from content review. Scope CSS with #shopify-section-{{ section.id }} or a BEM prefix tied to the section class so your component does not override Dawn's global button styles. In client work we duplicate the theme before the first deploy, name the preview theme with the ticket number, and only publish after the merchant signs off in the editor. That workflow sounds heavy for beginners, but it prevents the number one support issue: someone editing the live theme while you are still pushing commits.
Debugging Liquid when the editor preview lies
Shopify's theme editor sometimes caches section HTML aggressively. Hard refresh the preview iframe when schema changes do not appear. Invalid schema JSON fails silently until you open the code editor and save—the section may vanish from the Add section list. Use the theme check extension in VS Code or the Shopify CLI theme check command to catch deprecated tags and missing render arguments before deploy. When output is empty, log mentally: is the object nil on this template? collection is undefined on the homepage unless you assigned it. Is the setting id wrong? A typo in section.settings.heading_text versus heading breaks the binding. Is a filter returning blank? strip_html on an empty richtext field clears everything. Add temporary HTML comments with assign debug values during development, then remove before merge. The /shopify-cheat-sheet lists filters for default fallbacks—{{ section.settings.subheading | default: 'Shop the collection' }} keeps layouts from collapsing while merchants fill content.
Snippets: render, don't include
render loads snippets in an isolated scope. Variables from the parent template are not automatically visible unless you pass them as parameters. include is legacy and can cause name collisions when two snippets expect different values for the same variable name. Dawn and every modern theme use render exclusively. Parameters are explicit: {% render 'price', product: product, show_badges: true %}. Inside the snippet, reference product directly. Optional parameters should have sensible defaults using assign with or or the default filter. Keep snippets focused—one product card, one icon, one pagination row. Sections orchestrate; snippets execute small repeatable units. When you convert HTML that repeats three identical cards, the converter often creates blocks; when the repeat is structural (same card for related products), a snippet is the right abstraction.
Building confidence before your first client deploy
Beginners often ask whether they know enough Liquid to ship on a paid project. The honest bar is lower than you think if you master output tags, for loops, if blank checks, section.settings, and block loops. You do not need to memorize every filter—bookmark /shopify-cheat-sheet and learn image_url, money, default, and escape first. Your first deploy should be a duplicate theme, not production. Walk through the theme editor as if you were the merchant: change every field, reorder blocks, add a row, remove a row, preview mobile. If something breaks, trace whether the issue is nil data, wrong setting id, or CSS scope. Document what you fixed in a short handoff note—that note becomes your second article of expertise. Agencies hire for reliability, not encyclopedic Liquid knowledge. Reliability means presets work, schema validates, and merchants are not surprised by hardcoded text. Pair this guide with /shopify-section-tutorial when you move from snippets to full section files, and use /converter when static HTML arrives before you have mental schema mapped.
Liquid beginners become productive when they stop treating templates like static HTML and start thinking in data sources. Every visible string should answer: is this from Shopify (product.title), from the merchant (section.settings.heading), or from a repeatable row (block.settings.question)? If you cannot answer, the content will eventually be wrong for someone—usually the merchant who needs to edit it without filing a ticket. That discipline is what separates theme engineers from page builders. Over time you will internalize filters and loop options; until then, copy from /shopify-liquid-examples, compare converter output, and keep theme check clean. The learning curve is front-loaded; maintenance is easier once schema ids are stable and CSS is scoped. This is the same progression we followed on client stores before publishing HTML to Liquid Converter publicly.
Quick validation before your first deploy
- Run theme check on the duplicate theme — invalid schema or deprecated tags block a clean deploy
- Confirm every text output uses escape on merchant-controlled fields
- Open the theme editor on mobile preview after clearing optional images
- Verify presets appear in Add section with readable names, not generic defaults
- Compare setting ids in Liquid against schema JSON character for character

