Shopify Liquid Render Tag with Named Parameters

The Shopify Liquid render tag includes a snippet from the snippets folder and runs it inside an isolated scope, accepting named parameters, a single value via with X as Y, or an iterable via for X as Y. It replaces the deprecated include tag and is the only snippet inclusion mechanism supported in Online Store 2.0 themes. Render snippets cannot read parent template variables, which makes them predictable, testable, and safe to call inside loops or Section Rendering API responses.


If you have audited a Shopify theme built before 2020, you have probably seen {% include 'product-card' %} scattered through every collection template. Theme Check now flags every one of those calls, and Dawn does not ship a single include. The replacement is {% render %}, and once you understand the scope rules and the named parameter syntax, it becomes the single most useful tag in the language for keeping a theme maintainable. This is the reference I wish existed when I migrated my first include heavy theme to Online Store 2.0. For broader context on Liquid as a templating language, start with the Shopify Liquid development guide.

What is the Liquid render tag in Shopify?

The render tag is the modern Liquid construct for including a snippet file from the /snippets folder of a Shopify theme. It executes the snippet inside an isolated variable scope, returns the rendered HTML in place, and is the only inclusion tag recommended for Online Store 2.0 themes.

A snippet is any .liquid file inside /snippets. To call it, you reference it by filename without the extension:

{% render 'product-card' %}

This loads /snippets/product-card.liquid, runs it with no inputs, and inserts the result into the page. If the snippet references a variable that was not passed in, that variable is nil inside the snippet, even if the parent template defined it. That last part is the key behavioral change from include and the reason render exists.

The render tag accepts three input patterns: named arguments, a single value via with, and an iterable via for. You can combine with or for with named arguments in the same call, which is how most production snippets look in practice.

How is {% render %} different from {% include %}?

The two tags both load a snippet from /snippets, but render runs the snippet in an isolated scope where parent variables are invisible, while include runs the snippet in the parent scope and silently leaks every variable. Shopify deprecated include in 2019 and Theme Check now flags it as an error in Online Store 2.0 themes.

The differences at a glance:

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 required Implicit inheritance
Safe inside loops Yes, pure function of inputs Yes, but harder to reason about
Nested calls Cannot call include inside Can nest other includes
Used in Dawn Every snippet call Zero calls

Concrete example. Imagine the parent template has {% assign price = product.price %} and the snippet references {{ price }}. With include, the snippet prints the price. With render, the snippet prints nothing because price was never passed in.

{%- assign price = product.price -%}

{% comment %} Old behavior, snippet sees price {% endcomment %}
{% include 'price-block' %}

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

This isolation is a feature, not a regression. It means a render snippet is a pure function of its named inputs, so you can drop it into any section, any loop, or any Section Rendering API response and it will behave identically. The cost is that you have to be explicit about every input, which is a one time refactor pain that pays back forever.

If you are weighing whether to break a section into snippets at all, I covered the architecture trade off in the post on replacing apps with Liquid snippets.

How do I pass named parameters to a Liquid snippet?

Pass named parameters by listing them as comma separated key value pairs after the snippet name. Each parameter becomes a local variable inside the snippet under the same name. Parameters can be Liquid objects, strings, numbers, booleans, or expressions, and there is no upper bound on how many you can pass.

{% 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:

<article class="card">
  {%- if show_vendor -%}
    <p class="card__vendor">{{ product.vendor }}</p>
  {%- endif -%}

  <h3 class="card__title">{{ product.title }}</h3>

  <img
    src="{{ product.featured_image | image_url: width: image_width }}"
    width="{{ image_width }}"
    loading="lazy"
    alt="{{ product.featured_image.alt | escape }}"
  >

  {%- if badge_text != blank -%}
    <span class="card__badge">{{ badge_text }}</span>
  {%- endif -%}

  <p class="card__price">{{ product.price | money }}</p>
</article>

A few rules worth committing to memory. Parameter names must be lowercase with underscores, never camelCase, because Liquid lowercases identifiers internally. Boolean false and the string 'false' are different inside the snippet, so prefer real booleans. There is no built in default keyword on render arguments, but you can guard each value at the top of the snippet with {% assign image_width = image_width | default: 600 %}, which gives you the same effect.

What does with X as Y do in {% render %}?

The with keyword passes a single value into the snippet and exposes it under the chosen alias. It is shorthand for the most common case where the snippet renders one of something, like one product card or one media block, and the alias makes the snippet code read naturally regardless of what was passed in.

{% render 'product-card' with featured_product as product %}

Inside the snippet, the variable is named product, even though the parent passed featured_product. This is useful when the snippet is generic and the calling template happens to use a different name.

The pattern I use most often is in the Factory Direct Blinds builder, where a single configurator step renders a fabric swatch picker and the parent section holds the active step under a context specific name:

{%- 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 ever needs to know about step, so the alias keeps the snippet portable. 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.

What does for X as Y do in {% render %}?

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. It is the cleanest way to express loops where the body is a self contained snippet, and it generates one isolated scope per iteration, which keeps memory predictable.

{% render 'variant-card' for product.variants as variant %}

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

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

A real production example. On the Enea Studio build, the PDP renders one card per metal finish, and the gallery image swaps on click. The whole loop is one render call:

<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, so the section file stays under twenty lines. If you have ever written {% for variant in product.variants %}{% render 'metal-swap-card', variant: variant %}{% endfor %}, the for X as Y form is the same logic with one less indentation level and one less local assign. Performance is identical, but the loop with named arguments alongside is the form Shopify documents and Theme Check prefers.

For a deeper look at performance under heavy iteration, see Liquid loop optimization, which covers when to precompute filters versus letting them run inside the snippet.

How does Liquid operator precedence work inside a rendered snippet?

Liquid evaluates and and or operators strictly right to left with no precedence rule between them, which is the opposite of most programming languages where and binds tighter than or. This rule applies identically inside a rendered snippet, so wrapping logic in render does not change the evaluation order. The fix when you need precedence is to split compound conditions into nested if blocks or precompute booleans with assign.

This is the gotcha that bites every developer once. Take this expression:

{% if product.available and product.tags contains 'sale' or product.compare_at_price > product.price %}
  Show sale badge
{% endif %}

A C, JavaScript, or Ruby developer reads this as (available and on-sale-tag) or has-compare-at. Liquid reads it right to left as available and (on-sale-tag or has-compare-at). The two outcomes diverge whenever the product is unavailable but has a compare at price.

The reliable fix is to compute each piece into a named boolean first, which also makes the intent legible to anyone auditing the theme later:

{%- 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 -%}

{%- if show_badge -%}
  <span class="badge badge--sale">Sale</span>
{%- endif -%}

This pattern is verbose, and that is the point. Liquid is a templating language, not a logic language, so pushing complex boolean math into named flags before the render call keeps the snippet itself simple. When the snippet only sees show_badge, it has nothing to misinterpret.

Real example: a render-based product card snippet

Below is a complete, production grade product card snippet that uses every render feature covered in this post: named arguments, with X as Y at the call site, default value handling, money filter for price, image_url with width, and isolated scope so the snippet is safe inside any loop, section, or AJAX response.

The call site lives in a collection grid section:

<ul class="product-grid">
  {% paginate collection.products by 24 %}
    {% for product in collection.products %}
      <li>
        {% render 'product-card' with product as product,
          image_width: 600,
          show_vendor: settings.show_vendor,
          show_compare_at: true,
          badge_threshold_pct: 15
        %}
      </li>
    {% endfor %}

    {{ paginate | default_pagination }}
  {% endpaginate %}
</ul>

The snippet itself, /snippets/product-card.liquid:

{%- comment -%}
  Renders a single product card.
  Required: product
  Optional: image_width, show_vendor, show_compare_at, badge_threshold_pct
{%- endcomment -%}

{%- assign image_width = image_width | default: 600 -%}
{%- assign show_vendor = show_vendor | default: false -%}
{%- assign show_compare_at = show_compare_at | default: false -%}
{%- assign badge_threshold_pct = badge_threshold_pct | default: 10 -%}

{%- assign on_sale = false -%}
{%- if product.compare_at_price > product.price -%}
  {%- assign discount_pct = product.compare_at_price
    | minus: product.price
    | times: 100.0
    | divided_by: product.compare_at_price -%}
  {%- if discount_pct >= badge_threshold_pct -%}
    {%- assign on_sale = true -%}
  {%- endif -%}
{%- endif -%}

<article class="card" data-product-id="{{ product.id }}">
  <a href="{{ product.url }}" class="card__media">
    <img
      src="{{ product.featured_image | image_url: width: image_width }}"
      srcset="
        {{ product.featured_image | image_url: width: image_width }} 1x,
        {{ product.featured_image | image_url: width: image_width | times: 2 }} 2x
      "
      width="{{ image_width }}"
      height="{{ image_width }}"
      loading="lazy"
      alt="{{ product.featured_image.alt | default: product.title | escape }}"
    >

    {%- if on_sale -%}
      <span class="card__badge">Save {{ discount_pct | round }} percent</span>
    {%- endif -%}
  </a>

  <div class="card__body">
    {%- if show_vendor and product.vendor != blank -%}
      <p class="card__vendor">{{ product.vendor }}</p>
    {%- endif -%}

    <h3 class="card__title">
      <a href="{{ product.url }}">{{ product.title }}</a>
    </h3>

    <p class="card__price">
      {%- if show_compare_at and on_sale -%}
        <s class="card__price--was">{{ product.compare_at_price | money }}</s>
      {%- endif -%}
      <span class="card__price--now">{{ product.price | money }}</span>
    </p>
  </div>
</article>

This snippet has zero dependencies on the parent scope. Every variable it reads was either passed in as a named argument, computed locally with assign, or accessed through the global product object that came in via with. That means you can render the same card from a featured collection section, a search results page, an AJAX cart drawer cross sell, or a Section Rendering API response, and it will behave identically in all four contexts. That is the entire point of the render tag.

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 Theme Check flags it, the Online Store 2.0 documentation only references render, and Dawn ships zero include calls.

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. The trade off is reliability: render snippets are pure functions of their inputs.

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. The for keyword iterates a collection and renders the snippet once per item, exposing each item under the alias along with a forloop object.

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 call include inside itself.

How does Liquid evaluate and or operators inside a snippet? Liquid evaluates conditions strictly right to left with no precedence between and and or. The fix is to split compound conditions into nested if blocks or assign intermediate booleans before the if statement.

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 runtime can optimize repeated calls and your snippet stays pure.


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

Need a Liquid Developer Who Understands CRO?

I'll audit your theme code and show you exactly what's costing you conversions. 12+ years of Shopify Liquid experience across 100+ stores.

Get a Free Code Review

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