Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Going Global with Twig Globals in Craft CMS
Learn how to simplify some Twig templating challenges by leveraging the global variables & the _globals Collection added in Craft CMS 4.5
The Twig templating language used by Craft CMS is powerful, but it also has some intentional limitations and, like all programming languages, some quirks as well.
One example of this quirkiness is the scope of variables in Twig.
Variable scope refers to the blocks of code where a variable can be accessed and/or modified.
Global variables can be accessed anywhere
A global variable, then, is a variable that can be accessed from any code anywhere in your code base.
It’s not always as simple as you might expect to create a variable that is globally available in all of your templates in Twig.
To understand why, read the Twig Processing Order & Scope article for an in-depth discussion on this rather nuanced topic.
Link Globals are bad
Ask any veteran programmer about global variables, and you’ll likely get involved in a long storied discussion of their evil ways.
That’s because they’ve spent many hours attempting to fix problems that have arisen from globally scoped variables that can be accessed or changed from anywhere.
Globals aren’t evil, of course, but they allow for coding patterns that can be difficult to debug.
Anywhere your code accesses a global variable is a dependency on that variable, and anywhere your code changes a global variable could potentially be a breaking change for all of those dependencies.
That’s why things like dependency injection are used to build more complex systems, and many modern frontend languages use paradigms like props (think parameters or variables) that can only be passed down to components in a one-way data flow.
This dependency also makes code less modular, because if the code you write is inherently dependent on a global variable, it makes it difficult to re-use that code elsewhere.
All of that said, there are valid times when you need to use global variables, and many web sites are not complicated enough that you can get yourself into too much trouble.
Just be aware of the trade-offs, and try to avoid global variables as a design pattern if you can.
Link A Bad Example
Here’s a simplistic example of using global variables in a way that can get you in trouble. We have an index.twig template:
{% set width = 200 %}
{% set height = 100 %}
{% set color = 'red' %}
{% set title = 'My Cool Button' %}
{% include 'globals-example/button.twig' %}
So, we set some global variables, and then we include our button.twig template, which is our component that renders the button:
<button style="height:{{ height }}px; width:{{ width }}px; background-color:{{ color }};">
{{ title }}
</button>
There are some glaring problems with the way we’ve written this:
- The button.twig component is tightly coupled to the index.twig template, because it relies on the global variables height, width, color, and title being set there. This kills the reusability of our component; we can’t use it in another project by just copying the file over.
- We’re polluting the global variable namespace by using a bunch of fairly generic variable names that other components might want to use, too. For instance, what if we wrote another checkbox.twig component that also needed a height, width, color, and title?
- Because the variables the button.twig component needs are global variables, other code could inadvertently change them, causing side effects in how our button renders.
Let’s do a quick refactor to clean it up a bit to eliminate these issues:
{% include 'globals-example/button.twig' with {
width: 200,
height: 100,
color: 'red',
title: 'My Cool Button'
} only %}
Instead of setting the variables our component needs in the global namespace, we pass them into our component.
This eliminates all of the issues we called out in our bullet points above:
- The index.twig and button.twig are no longer tightly coupled. We can just copy our button.twig component to re-use it in another project.
- We are no longer polluting the global variable namespace. The variables only exist in the context of our button.twig component.
- We avoid side effects because other code can no longer inadvertently modify the variables our button.twig component needs.
While our example is simple, the more complicated your codebase becomes the more troublesome some of the issues we’ve mentioned become.
Link Globals in Craft
Alright, so we know global variables are often a bad idea, but I trust you to use them judiciously, so let’s talk about doing just that.
When discussing global variables in this article, we are not talking about Craft Globals, which allow you to make content available globally in your templates.
Instead, we’re talking about Twig variables that are globally accessible.
A trick some have learned is that you can make a variable globally accessible to all of your Twig templates by putting it in a layout that every other template extends from:
{% set someGlobal = "woof" %}
Then you might have a layout template that extends from your _globals.twig template:
{% extends _globals.twig %}
{% block content %}
{% endblock content %}
And now any template that extends your _layout.twig template will get any global variables defined in _globals.twig, all the way down the chain.
This lets you nicely side-step some of the weirdness involved with Twig Processing Order & Scope.
But still, there are some limitations such as child templates not being able to modify global variables.
Due to the way Twig works, templates that {% extends %} or {% include %} or {% embed %} other templates can inherit global variables (called the context in Twig parlance), but they can’t change them.
Twig templates are passed in a copy of the Twig context when they are rendered, but any changes to that context are discarded when it is done rendering.
Sidenote: If you ever want to see the Twig context (aka all of the available variables) in the currently rendering template, you can do so via: {{ dump(_context) }}
This is an intentional design decision in Twig, to help mitigate some of the dependency complications with global variables that we discussed above.
However, there are cases where it can be frustrating.
This is one of the reasons why is why in Craft 4.5 they introduced the _globals global.
Link Leveraging _globals
So what’s the point of this new _globals thing in Craft 4.5 and later, and why would we use it?
_globals is just another Twig global variable that is always available in the Twig context that your templates render.
Yes, _globals is a global variable
Under the hood, it’s actually a Laravel Collection object, so all of the Collection methods are available, so you can use it as a key-value store for whatever data you like.
Here are some examples from the Craft CMS docs on using the _globals variable:
{% do _globals.set('theme', 'dark') %}
{% do _globals.set({
red: '#F00',
green: '#0F0',
blue: '#00F',
}) %}
{{ _globals.get('theme') }}
{# -> 'dark' #}
{{ _globals.get('red') }}
{# -> '#F00' #}
Since _globals is just a Twig global variable, you might wonder why it even exists. You could just create it yourself in your _globals.twig template that everything else extends via something like:
{% set _globals = {
theme: 'dark',
red: '#F00',
green: '#0F0',
blue: '#00F',
} %}
This is roughly analogous to what Craft does under the hood to inject the _globals variable, with two key distinctions:
- Since Craft is injecting this global variable, it is always in the Twig context when your templates are rendered. Whereas if you include other templates via {% include 'template.twig' only %} or {% include 'template.twig' with {'foo': 'bar'} only %}, variables you’ve set in your _globals.twig template (or elsewhere) will not. The _globals Collection will also be available in Element Templates that are rendered with the new Element .render() method added in Craft 5.
- Because this is a Collection object, you can change the values that are in it from other templates, and the changes will persist as the rest of your templates render.
To illustrate the difference, let’s first have a look at some templates that use the technique of setting a global in your layout.twig template that we mentioned earlier:
{% set color = 'green' %}
{% block header %}
{% endblock header %}
{% block content %}
{% endblock content %}
{% set color = 'blue' %}
{% extends 'globals-example/layout.twig' %}
{% set color = 'red' %}
{% block header %}
{{ color }}
{% include 'globals-example/include.twig' only %}
{% endblock %}
{% block content %}
{{ color }}
{% endblock %}
What do you think the output of the above template will be?
Think about it for a minute, then scroll down.
↓
↓
↓
↓
↓
…the output is probably not what you’d expect:
green green
Didn’t we set the color global variable to red in our index.twig template, and then set it to blue in our include.twig template? Why isn’t the output red blue?
This is because the code in the layout.twig template is processed after the code in the index.twig template, so while the color variable was changed to red, it was then set to green before the block renders.
Then in our include.twig file, the color variable was changed to blue, but only in the copy of the context that was passed down to it, which is discarded after it is rendered. So we end up with a rather unintuitive result of green green.
Let’s take a look at how we can use the _globals Collection to make this a bit better:
{% do _globals.set('color', 'green') %}
{% block header %}
{% endblock header %}
{% block content %}
{% endblock content %}
{% do _globals.set('color', 'blue') %}
{% extends 'globals-example/layout.twig' %}
{% do _globals.set('color', 'red') %}
{% block header %}
{{ _globals.get('color') }}
{% include 'globals-example/include.twig' only %}
{% endblock %}
{% block content %}
{{ _globals.get('color') }}
{% endblock %}
What do you think the output of the above template will be?
Think about it for a minute, then scroll down.
↓
↓
↓
↓
↓
…the output is probably not what you’d expect:
green blue
Well, at least this is a little closer to what we’re looking for, setting the color item in the _globals Collection in our include.twig template did stick and set it to blue.
But didn’t we set the color item in our _globals Collection to red? Why isn’t the output red blue?
Again, this is because the code in the layout.twig template is processed after the code in the index.twig template, but before the code in the blocks in the index.twig template.
We can work around this by adding an indexCode block to our layout.twig & index.twig Twig templates:
{% do _globals.set('color', 'green') %}
{% block indexCode %}
{% endblock indexCode %}
{% block header %}
{% endblock header %}
{% block content %}
{% endblock content %}
{% extends 'globals-example/layout.twig' %}
{% block indexCode %}
{% do _globals.set('color', 'red') %}
{% endblock %}
{% block header %}
{{ _globals.get('color') }}
{% include 'globals-example/include.twig' only %}
{% endblock %}
{% block content %}
{{ _globals.get('color') }}
{% endblock %}
This causes the code to be executed in the order we’re looking for, making the output:
red blue
Link A Good Example
Let’s look at a real-world example where you might use global variables to change the color theme of your website.
Let’s not get hung up on whether you think doing theming via Twig rather than CSS is a good idea (though you very well might if you’re using Tailwind CSS); the goal is to provide an example where leveraging the _globals Collection is useful.
Here are the templates:
{% do _globals.set('currentTheme', 'dark') %}
{% do _globals.set('themes', {
dark: {
foregroundColor: 'LightBlue',
backgroundColor: 'DarkRed'
},
light: {
foregroundColor: 'MidnightBlue',
backgroundColor: 'Salmon'
},
}) %}
Here we are adding two items to the _globals Collection, currentTheme which is the name of the theme that is currently selected, and themes which defines our light and dark themes, each with a foregroundColor and backgroundColor property in them.
N.B.: My color scheme choices are atrocious. I never claimed to be a designer 🤭
{% include 'theme-example/themes.twig' %}
{% set requestCookies = craft.app.request.cookies %}
{% set themeCookie = requestCookies.get('currentTheme') %}
{% if themeCookie and themeCookie.value != '' %}
{% do _globals.set('currentTheme', themeCookie.value) %}
{% endif %}
{% block header %}
{% endblock header %}
{% block content %}
{% endblock content %}
Our layout.twig template imports our themes.twig template, and then might change the currentTheme value in the _globals Collection if it’s set in a cookie.
For more on accessing cookies via Twig, check out the Cutting the Cord: Removing Craft CMS 3 Plugins article.
This cookie would probably be set via some user preference via frontend JavaScript. Alternatively, a custom field on the User element in Craft could be used to indicate their preference.
Or you could just set the value based on the current time of day where they happen to be, or the browser color scheme preference.
Whatever floats your boat!
{%- macro getCurrentThemeValue(value) -%}
{%- set currentThemeName = _globals.get('currentTheme', 'dark') -%}
{%- set theme = _globals.get('themes')[currentThemeName] -%}
{{- theme[value] -}}
{%- endmacro -%}
Our macros.twig template defines the getCurrentThemeValue() macro which makes it more convenient to pull out the value of a property from the current theme. This is something we might be doing often, so it makes sense to create a macro for it.
<button
style="height:{{ height }}px; width:{{ width }}px; color:{{ foregroundColor }}; background-color:{{ backgroundColor }};">
{{ title }}
</button>
Next, we have a button.twig template that renders a button for us. This is mostly the same as presented earlier, except that we’re allowing for both foreground & background colors to be passed into it.
{% extends 'theme-example/layout.twig' %}
{% from 'theme-example/macros.twig' import getCurrentThemeValue %}
{% block header %}
{% include 'theme-example/button.twig' with {
width: 300,
height: 50,
foregroundColor: getCurrentThemeValue('foregroundColor'),
backgroundColor: getCurrentThemeValue('backgroundColor'),
title: 'Header Button'
} only %}
{% endblock %}
{% block content %}
{% include 'theme-example/button.twig' with {
width: 200,
height: 100,
foregroundColor: getCurrentThemeValue('foregroundColor'),
backgroundColor: getCurrentThemeValue('backgroundColor'),
title: 'Content Button'
} only %}
{% endblock %}
And finally, we have our index.twig template that imports our getCurrentThemeValue() macro, and then includes the button.twig component template to render our button.
This is a case where we really do want the theme to be globally available, but we’ve done so in a way that makes it accessible everywhere in our templates while still not being tightly coupled to the component templates that we are rendering.
We’re still just passing values into our template components, but we’re fetching them from a globally defined theme that can be changed as needed.
And because we’re using the _globals Collection, we could change the theme on a per-template basis with something like:
{% do _globals.set('currentTheme','dark' %}
…in a {% block %} in the template, or in an {% include %} and still have it work.
Link Getting Global
Hopefully this article has helped you understand the pitfalls of global variables in Twig, and a bit about how things work under the hood.
This should help you judiciously use global variables — and leverage the Craft _globals global variable — effectively in your projects.
Happy globe-trotting!