Supercharge Macros in Craft CMS
We first wrote this in 2016, on Craft 2. Ten years and three major versions later, Craft 5 runs on Twig 3, and we still build templates around the same idea. Here it is, brought up to date.
Twig macros are how you keep Craft templates DRY. A card, a button, a media block: anywhere you repeat markup with small variations, a macro or an include saves you copying it around.
The problem with positional arguments
Say you write a card macro with four arguments:
{% macro card(title, body, url, image) %}
Callers have to remember the order. Come back to it a few weeks later and you're counting commas. Skip url but pass image, and you end up with this:
{{ ui.card('Title', 'Body', null, image) }}
It's hard to read and easy to get wrong.
Pass one hash, with defaults
Instead, take a single hash of attributes and merge it over a set of defaults. Jeremy Daalder suggested this to us back in 2016, and it's held up:
{% macro card(attrs) %}
{% set attrs = {
title: '',
body: '',
url: null,
image: null,
}|merge(attrs) %}
<article class="card">
{% if attrs.image %}
<img src="{{ attrs.image.url }}" alt="{{ attrs.image.alt }}">
{% endif %}
<h3>
{% if attrs.url %}
<a href="{{ attrs.url }}">{{ attrs.title }}</a>
{% else %}
{{ attrs.title }}
{% endif %}
</h3>
<p>{{ attrs.body }}</p>
</article>
{% endmacro %}
Calling it reads like a description of the card:
{% import '_macros/ui' as ui %}
{{ ui.card({
title: entry.title,
url: entry.url,
image: entry.featuredImage.one(),
}) }}
Each value is named, callers pass only what they need, and the defaults at the top double as documentation. No more is defined checks scattered through the template.
Two things to know in Twig 3
Imports don't inherit. Macros you import are local to that template. They're available in its blocks, but not in templates it includes or templates that extend it, so import them in each template that uses them.
Macros only see what you pass in. A macro has its own scope, so template variables like entry aren't available inside it unless you pass them. That's a feature: a macro that only depends on its arguments behaves the same wherever you call it.
When an include is the better tool
For bigger components we now use includes rather than macros, with the same defaults pattern. Every component on this site is built this way. Here's the top of our button:
{# _components/button.twig #}
{% set defaults = {
label: '',
href: '#',
variant: 'primary',
size: 'md',
} %}
{% set params = defaults|merge(params ?? {}) %}
And calling it:
{% include '_components/button' with {
params: { label: 'Back to the homepage', href: '/', size: 'lg' }
} only %}
The only keyword stops the component seeing anything from the template that called it, so, like a macro, it behaves the same wherever it's used. When a component needs a slot for arbitrary markup, you can swap the include for an {% embed %} and override a block, which macros can't do.
Our rule of thumb: macros for small helpers that return a snippet of markup or a formatted value, includes for anything component-sized.
Sources
Andrew is Technical Director at Mutual, a Craft CMS Partner agency. He has been building with Craft CMS since its public beta in 2012 — working through every major version from Craft 1 to Craft 5 — and has delivered over 100 sites for clients including Apple, Transparency International, and Arts University Bournemouth.
He writes about Craft CMS on the Mutual blog and has contributed to net Magazine. At Mutual, he leads development of Mutual One, a marketing platform built on Craft CMS as its foundation.
He has spoken about Craft CMS to undergraduate students at the University of Brighton and Canterbury Christ Church University, and appeared on the Devmode.fm podcast. He has also trained other developers on working with the platform.
More from the blog
Craft CMS and PHP2Shell - how we dealt with it
How Mutual (a Craft CMS agency) handled the PHP2Shell vulnerability.
What's happened to my ChatGPT traffic?
You might have seen a drop in traffic from ChatGPT from this week. Here's what's happening...
The Best Craft CMS Learning Resources in 2026
Where we'd send a developer learning Craft CMS in 2026: the official docs and tutorial, CraftQuest, the Discord, the newsletter and the conference talks.