Liquid Render Tag Syntax: with, for, Named Params (2026)

I audited a 2019-era Shopify theme last quarter for a Factory Direct Blinds migration. Every collection template still ran {% include 'product-card' %}. Theme Check threw 47 errors before the build even finished. That single tag swap is the cleanest performance win I ship on legacy themes.

TL;DR: The Liquid render tag loads a snippet inside an isolated scope and accepts named parameters, a single value via with X as Y, or an iterable via for X as Y. It replaced include in 2019 and is the only inclusion tag Online Store 2.0 supports. Snippets cannot read parent variables, which makes them safe inside loops and Section Rendering API responses.

Why this matters for your store

  • Replacing include with render clears every Theme Check error that blocks Shopify theme store submission.
  • Isolated scope makes snippets reusable across PDP, search, AJAX cart, and Section Rendering API responses with zero rewrites.
  • Pure-function snippets cut debug time on collection pages with 50+ product cards.

The behavior split is older than most agency devs realize. Shopify shipped render in late 2019 alongside Online Store 2.0 sections, and Dawn (the reference theme since 2021) has zero include calls. If you inherit a theme built on Debut, Brooklyn, or any Slate-era boilerplate, expect every snippet call to be include. Migrate that pattern first, before any layout or schema work.

Shopify dev docs reference page for the Liquid render tag describing snippet inclusion, isolated scope, named parameters, with X as Y, and for X as Y syntax

What the render tag actually does

A snippet is any .liquid file inside /snippets. You call it by filename, no extension:

{# layout/theme.liquid #}
{% render 'product-card' %}

That loads /snippets/product-card.liquid, runs it with no inputs, and inserts the output. If the snippet references a variable the parent did not pass, that variable is nil inside the snippet. Even if the parent template defined it. That isolation is the whole reason render exists.

You can pass three input shapes: named arguments, a single value via with, or an iterable via for. You can combine with or for with named arguments in the same call. Most production snippets do.

For broader Liquid context, my Shopify Liquid development guide covers the language fundamentals.

Why include leaks variables and render does not

Both tags load files from /snippets. The difference is scope.

Behavior {% render %} {% include %}
Scope Isolated, parent variables invisible Parent scope, every variable leaks in
Status Recommended for OS 2.0 Deprecated since 2019
Theme Check Passes Flags as error
Variable passing Explicit named parameters Implicit inheritance
Used in Dawn Every snippet call Zero calls

Concrete demo. Parent template assigns price, snippet references {{ price }}:

{# templates/product.liquid #}
{%- assign price = product.price -%}

{# Old: snippet sees price via leak #}
{% include 'price-block' %}

{# Modern: snippet sees nothing unless passed #}
{% render 'price-block', price: price %}

Isolation is a feature. A render snippet behaves as a pure function of its named inputs, so you can drop it into any section, any loop, or any Section Rendering API response without surprise. The cost is one explicit refactor. It pays back forever.

If you are weighing whether to break a section into snippets, I covered that trade-off in replacing apps with Liquid snippets.

How to pass named parameters cleanly

List parameters as comma-separated key value pairs after the snippet name. Each becomes a local variable under the same name:

{# sections/featured-collection.liquid #}
{% render 'product-card',
  product: product,
  show_vendor: true,
  image_width: 600,
  badge_text: 'New In'
%}

Inside /snippets/product-card.liquid, those four names are addressable directly. Three rules I commit to memory: parameter names must be lowercase with underscores (Liquid lowercases identifiers internally), boolean false and the string 'false' behave differently inside the snippet, and there is no built-in default keyword on render arguments. Guard each value at the top of the snippet:

{# snippets/product-card.liquid #}
{%- assign image_width = image_width | default: 600 -%}
{%- assign show_vendor = show_vendor | default: false -%}

Same effect as a default keyword, zero ambiguity.

When to use with X as Y

The with keyword passes one value into the snippet under a chosen alias. It reads naturally when the snippet renders one of something:

{# sections/featured-product.liquid #}
{% render 'product-card' with featured_product as product %}

Inside the snippet, the variable is product, even though the parent passed featured_product. The alias keeps the snippet generic.

Shopify builder v2 desktop configurator with modular sections on Factory Direct Blinds, where each step renders a swatch picker snippet via render with X as Y

I lean on this pattern in the Factory Direct Blinds builder. One configurator step renders a fabric swatch picker. The parent section feeds the active step under a context-specific name:

{# sections/builder-v2.liquid #}
{%- assign current_step = builder.steps[step_index] -%}

{% render 'builder-swatch-picker' with current_step as step,
  builder_id: builder.id,
  show_prices: true
%}

The snippet only knows about step. The same builder-swatch-picker snippet runs across fabric, valance, and lining steps because the parent picks which step object to feed it via with. One snippet, three call sites.

When to use for X as Y

The for keyword iterates a collection and renders the snippet once per item, exposing each item under the alias plus a forloop object scoped to that iteration:

{# sections/main-product.liquid #}
{% render 'variant-card' for product.variants as variant %}

Inside /snippets/variant-card.liquid you read both variant and forloop:

{# snippets/variant-card.liquid #}
<li class="variant-card" data-index="{{ forloop.index0 }}">
  <span class="variant-card__title">{{ variant.title }}</span>
  <span class="variant-card__price">{{ variant.price | money }}</span>
  {%- if forloop.last -%}
    <span class="variant-card__last-flag">Last variant</span>
  {%- endif -%}
</li>

Shopify dynamic metal swap Liquid description on the Enea Studio PDP showing the 18k yellow gold variant rendered through a render for X as Y loop on product variants

Production example. On the Enea Studio PDP, the metal-swap component renders one card per finish. Gallery image swaps on click. The whole loop is one render call:

{# sections/main-product.liquid #}
<ul class="metal-swap" data-product-id="{{ product.id }}">
  {% render 'metal-swap-card'
    for product.variants as variant,
    show_price: true,
    image_width: 1200
  %}
</ul>

The snippet handles its own active state, image preload, and ARIA labels. The section file stays under twenty lines. The equivalent {% for variant in product.variants %}{% render ... %}{% endfor %} works identically, but the for X as Y form is what Shopify documents and Theme Check prefers.

For deeper performance reading under heavy iteration, see Liquid loop optimization.

One performance note from the Enea build. The first version of the loop called image_url: width: variant.featured_image.width inside the snippet, which forced Shopify to compute a unique CDN URL per variant per render. Moving the width to a fixed 1200 and letting the CDN handle DPR via srcset cut Time to First Byte on the PDP from 480ms to 310ms over a 14-day measurement window in May 2026. Precompute or hard-code anything expensive before the snippet sees it.

The operator precedence bug that bites every dev once

Liquid evaluates and and or strictly right to left, with no precedence between them. The opposite of C, JavaScript, and Ruby, where and binds tighter than or. Wrapping logic in render does not change this. The full mechanics, the silent-parentheses trap, and the named-boolean fix get the deeper treatment in Shopify Liquid operator precedence: and / or evaluation order.

Take this line:

{# snippets/sale-badge.liquid #}
{% if product.available and product.tags contains 'sale' or product.compare_at_price > product.price %}
  Show sale badge
{% endif %}

A JavaScript developer reads this as (available AND has-sale-tag) OR has-compare-at. Liquid reads it right to left as available AND (has-sale-tag OR has-compare-at). The two outcomes diverge whenever the product is unavailable but has a compare-at price, which is most of an end-of-season catalogue.

The reliable fix: compute each piece into a named boolean first.

{# snippets/sale-badge.liquid #}
{%- assign has_sale_tag = product.tags contains 'sale' -%}
{%- assign has_compare_price = product.compare_at_price > product.price -%}
{%- assign show_badge = false -%}

{%- if product.available -%}
  {%- if has_sale_tag or has_compare_price -%}
    {%- assign show_badge = true -%}
  {%- endif -%}
{%- endif -%}

Verbose, and that is the point. Liquid is a templating language, so push complex boolean math into named flags before the render call. When the snippet only sees show_badge, it has nothing to misinterpret.

How to verify your render refactor in 5 minutes

  1. Run shopify theme check from the theme root. Every include should surface as LiquidTag or DeprecatedFilter. Zero passes means you are clean.
  2. Open Chrome DevTools on a collection page, throttle to Slow 4G, and reload. Note Largest Contentful Paint. Swap any remaining include for render with explicit args, redeploy, reload. LCP should hold or drop. If it climbs, you forgot to pass a precomputed image_url value.
  3. Hit a Section Rendering API endpoint such as /?section_id=featured-collection and confirm the response renders identically to the static page. Render snippets pass this test by default. Include-based snippets often fail because they relied on a leaked variable.

The takeaway

  • Replace every {% include %} with {% render %} and pass variables explicitly.
  • Use with X as Y when the snippet renders one of something and you want a generic alias.
  • Use for X as Y when the snippet itself is the loop body, and let Liquid manage iteration.
  • Precompute and/or logic into named booleans before the render call. Right-to-left evaluation is the default.
  • Verify with Theme Check, an LCP measurement, and one Section Rendering API request before you call it shipped.

Need a Liquid refactor or audit? Book a free 30-minute strategy call.

Frequently Asked Questions

Is the Liquid include tag deprecated in Shopify?

Yes. Shopify deprecated the include tag in 2019 and recommends render for all new theme work. Include is still parsed by themes for backwards compatibility, but the Theme Check linter flags it, the Online Store 2.0 documentation only references render, and Dawn ships zero include calls. Any theme audited for performance or store migration should replace every include with render and verify that snippets do not depend on parent scope variables.

Can a snippet rendered with render access parent template variables?

No. The render tag creates an isolated scope, so a snippet cannot read variables defined in the parent template unless they are passed in as named parameters. This is the largest behavioral break from include, which silently inherited every variable in scope. The trade-off is reliability: render snippets are pure functions of their inputs, which makes them safe to use inside loops, partial caches, and Section Rendering API responses.

What is the difference between with X as Y and for X as Y in render?

The with keyword passes a single value into the snippet under a chosen alias, useful for rendering one product card or one media gallery. The for keyword iterates a collection and renders the snippet once per item, exposing each item under the alias along with a forloop object scoped to that loop. Use with for one-shot rendering, use for when the snippet itself is the loop body and you want Liquid to manage iteration.

Does render support all Liquid filters and tags inside the snippet?

Yes, with two caveats. The snippet has no access to the parent forloop object unless you pass it explicitly, and the snippet cannot use the include tag inside itself per Theme Check rules. All filters work as expected, including money, image_url, and asset_url. Section level objects like section.settings are not available unless passed in as named arguments, which is intentional and helps make snippets portable across sections.

How does Liquid evaluate and or operators inside a snippet?

Liquid evaluates conditions strictly right to left with no operator precedence between and and or. This contradicts most programming languages, where and binds tighter than or. The fix is to split compound conditions into nested if blocks or to assign intermediate booleans before the if statement. This rule applies inside rendered snippets exactly as it does in the parent template, so the isolated scope does not change evaluation order.

Should I use render inside a paginate loop or a long for loop?

Yes, render is the correct choice inside loops. Because render creates an isolated scope, the Liquid runtime can optimize repeated calls and your snippet stays pure. For collection pages with **50 product cards**, a render based card is faster than an include based card and easier to debug. The only performance gotcha is calling expensive filters like image_url with custom widths inside the snippet rather than precomputing them.

Book Strategy Call