Shopify Variable Scopes

The good, the quirky and the downright hacky

Note - A few months after this was posted the render tag was released in Liquid. This post hasn't yet been updated to reflect the impact of this tag.


Recently I’ve been investigating how variable scoping works with Liquid templates, in Shopify themes specifically. Whilst they are generally pretty straightforward, I noticed some undocumented behaviours that seem worth sharing.

To my knowledge there isn’t any documentation on how Liquid variables are scoped, so I’m going to be using my own names for the different scopes that I see, but they should make sense.

The Global Scope

The global scope is accessible by all templates used to build your page. For example, on a product page, the product object is available to be used by any layouts, templates, sections or snippets. Similarly on a collection page, the collection object is available to be used. The global scope is important in this regard. It provides variables (which are typically objects) to be used anywhere on the page. All liquid files need to be able to access this scope so that they can render the content required for each page. I would also consider the global settings accessed via settings.[setting] to be a part of the global scope as they are available anywhere.

Page Scope

The page scope differs from the global scope in that only the layout files, template files and any snippets included directly from those files have access to it. All variables in the page scope are also provided via these files. For example, the following line: {% assign scope = ‘page’ %}, would create a scope variable that can be used anywhere on the page after it has been declared (except inside sections), including snippets that are added within either the layout or template files.

Section Scope

Shopify sections have their own private scope and cannot access variables that are created within the page scope. If you assign number to 12 and then include a section file immediately afterwards, you will not have access to that number variable inside the section. At the same time, you cannot assign variables inside a section which are then used within the page scope.

Snippet Scope

The variables accessible by snippets changes depending on where the snippet is included with the page. For example, if I add {% include ‘related-products.liquid' %} within my template file, related-products.liquid will have access to the page scope. However, if I include that snippet within a section file, the snippet instead has access to the section scope as opposed to the page scope.

Snippets also have their own private scope, which you declare by passing arguments into your snippets within the include tag. The method for doing so looks like this: {% include 'written-by', author: article.author %}. As you can see, within the include tag, arguments are passed in the following format: [key]: [value]. Multiple arguments can be passed into the snippet scope so long as they are comma separated within the include tag.

Variables within the snippet scope cannot be reassigned, which causes an interesting behaviour, where a variable can be reassigned within the variable scope, whilst being unchanged with the snippet scope. Take the following example:

{% comment %}
  article.liquid
{% endcomment %}
{% include 'written-by', author: 'John Bailey' %}
{{ author }}

{% comment %}
  written-by.liquid
{% endcomment %}
{% assign author = 'Derek Bailey' %}
Article written by {{ author }}

That example would alter the author variable within the page scope, but not the snippet scope, despite author being reassigned before being printed within the snippet and so this would be rendered:

Article written by Joe Bailey
Derek Bailey

You can even assign the page-scoped variable with the snippet-scoped variable as an argument within the assignment operator. For example:

{% comment %}
  index.liquid
{% endcomment %}
{% include 'multiply', number: 12, multiplier: 2 %}
{{ number }}

{% comment %}
  multiply
{% endcomment %}
{% assign number = number | times: multiplier %}
{{ number }} * {{ multiplier }} =

Would print:

12 * 2 = 
24

Moving variables between private scopes

By making use of the capture tag, it is possible to both inject global variables into private scopes and take privately scoped variables out of those scopes.

Injecting page variables into sections

The capture tag is similar to assign in that it is used for creating variables, however it essentially assigns the variable by cutting the content between its opening and closing tags. For example, the code below would assign the variable hello to Hello there!.

{% capture hello %}Hello there!{% endcapture %}

This also works content that passed in via include or section tags. So, by writing placeholder content inside of a section, we can use Liquids replace modifier to insert a variable from the page scope. Take this code for example:

{% comment %}
  section
{% endcomment %}
<== inject-here ==>

{% comment %}
  page
{% endcomment %}
{% assign page_variable = 'Yellow' %}
{% capture example %}
  {% section 'section' %}
{% endcapture %}
{{ example | replace: '<== inject-here ==>', page_variable }}

The code above assigns the variable example to the code that the section prints. Then, by printing the section on the page, we can replace the placeholder within the section with page_variable which is defined within the page scope.

Extracting section variables

You may have found a situation in which you need to be able to make use of the flexibility of blocks in a more global context. Blocks are extremely useful because they are repeatable and easily iterable, but unfortunately they cannot be created within the global settings.

To get around this, you can loop through the blocks, printing their settings values in strings, then extract those values from the strings.

{% comment %}
  section
{% endcomment %}
{% for block in section.blocks %}
  title:x,author:y,publishYear:z<br>
{% endfor %}

{% comment %}
  page
{% endcomment %}
{% capture section %}
  {% section 'section' %}
{% endcapture %}
{% assign blocks = section | split: '<br>' %}

{% for block in blocks %}
  {% assign settings = block | split: ',' %}
  {% for setting in settings %}
    {% assign keyval = setting | split: ':' %}
    <input
      id="{{ keyval.first }}"
      value="{{ keyval.last }}"
    >
  {% endfor %}
{% endfor %}