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