Grav Twig Macros — Reusable Template Blocks and Imports
In this tutorial, you'll learn Grav Twig macros — how to create reusable template blocks with parameters, import macros across templates, and compose complex UIs from simple macro components.
What You'll Learn
- What Twig macros are and when to use them
- Defining macros with
{% macro %}blocks - Macro parameters, defaults, and named arguments
- Importing macros with
{% import %}and{% from %}imports - Using macros across multiple templates
- Best practices for macro organization and reusability
Why It Matters
In WordPress, reusable template parts often require PHP functions or include files with duplicated logic. In Grav, Twig macros let you define a reusable UI component once and use it anywhere. Think of macros as template functions: they accept parameters, return HTML, and can be organized into library files. When you need to update a button style across 50 templates, you change one macro definition instead of 50 template files.
Real-World Use
A documentation site uses a "card" component in 12 different templates: blog listings, product cards, team members, documentation navigation, and search results. Each card has a title, description, image, and link. Instead of duplicating the card HTML in each template, the team defines it as a macro in a single file and imports it wherever needed.
Learning Path
flowchart LR
A["Twig Filters & Functions"] --> B["Twig Macros
← You are here"]:::current
B --> C["Twig Inheritance"]
C --> D["Twig Debugging"]
D --> E["Custom Twig Extensions"]
E --> F["Theme Configuration"]
F --> G["Theme Assets"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
What Is a Twig Macro?
A macro is a reusable template fragment defined with {% macro %}. It works like a function:
{% macro input(name, value, type) %}
<input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value }}" />
{% endmacro %}
You call the macro to render the fragment:
{{ _self.input('username', '', 'text') }}
{{ _self.input('email', 'user@example.com', 'email') }}
Defining Macros
Macros are defined with {% macro %} and closed with {% endmacro %}:
{% macro button(label, url, style) %}
<a href="{{ url }}" class="btn btn-{{ style|default('primary') }}">
{{ label }}
</a>
{% endmacro %}
Parameters and Defaults
{% macro card(title, body, image, options) %}
{% set class = options.class|default('card-default') %}
{% set link = options.link|default('#') %}
<article class="card {{ class }}">
{% if image %}
<img src="{{ image }}" alt="{{ title }}" />
{% endif %}
<h3>{{ title }}</h3>
<p>{{ body }}</p>
<a href="{{ link }}">Read more</a>
</article>
{% endmacro %}
Call with:
{{ _self.card(
'Getting Started',
'Learn how to install and configure Grav.',
'/images/grav-logo.png',
{ class: 'featured', link: '/docs/getting-started' }
) }}
Importing Macros
Macros defined in the same template use _self as the prefix. To share macros across templates, define them in a separate file and import them.
Create a Macro Library
Create user/themes/mytheme/templates/macros/forms.twig:
{% macro input(name, value, type, attrs) %}
<input
type="{{ type|default('text') }}"
name="{{ name }}"
value="{{ value }}"
class="{{ attrs.class|default('form-input') }}"
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}
{% if attrs.required %}required{% endif %}
/>
{% endmacro %}
{% macro textarea(name, value, attrs) %}
<textarea
name="{{ name }}"
class="{{ attrs.class|default('form-input') }}"
rows="{{ attrs.rows|default(4) }}"
>{{ value }}</textarea>
{% endmacro %}
{% macro select(name, options, selected, attrs) %}
<select name="{{ name }}" class="{{ attrs.class|default('form-input') }}">
{% for value, label in options %}
<option value="{{ value }}" {% if value == selected %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
{% endmacro %}
Import in a Template
{% import 'macros/forms.twig' as forms %}
<form>
{{ forms.input('username', '', 'text', {
placeholder: 'Enter your username',
required: true
}) }}
{{ forms.textarea('bio', '', {
rows: 6,
class: 'form-input bio-field'
}) }}
{{ forms.select('country', {
'us': 'United States',
'uk': 'United Kingdom',
'in': 'India'
}, 'us') }}
<button type="submit">Submit</button>
</form>
Selective Import
Import only specific macros:
{% from 'macros/forms.twig' import input, textarea %}
{{ input('email', '', 'email', { placeholder: 'Your email' }) }}
{{ textarea('message', '', { rows: 8 }) }}
Macros with Context Access
Macros do not have access to the template context by default. Pass context explicitly:
{% macro post_card(post) %}
<article class="post-card">
<h2>{{ post.title }}</h2>
<time>{{ post.date|date('F j, Y') }}</time>
<p>{{ post.summary }}</p>
<a href="{{ post.url }}">Read more</a>
</article>
{% endmacro %}
{% import 'macros/post.twig' as post_macros %}
{% for item in page.collection %}
{{ post_macros.post_card(item) }}
{% endfor %}
Macro Organization Patterns
Single Macro File
user/themes/mytheme/templates/macros/
├── forms.twig
├── cards.twig
├── navigation.twig
├── media.twig
└── utilities.twig
Import Strategy
{# Import everything from a module #}
{% import 'macros/cards.twig' as cards %}
{# Import selectively #}
{% from 'macros/utilities.twig' import icon, badge, tooltip %}
Advanced Macro Patterns
Recursive Macros
Macros can call themselves for nested structures:
{% macro menu(items, level) %}
{% set level = level|default(0) %}
<ul class="menu-level-{{ level }}">
{% for item in items %}
<li>
<a href="{{ item.url }}">{{ item.title }}</a>
{% if item.children %}
{{ _self.menu(item.children, level + 1) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
Macros with Blocks
While macros cannot use Twig blocks, they can accept template strings:
{% macro card_with_footer(title, content, footer) %}
<div class="card">
<div class="card-header">{{ title }}</div>
<div class="card-body">{{ content }}</div>
<div class="card-footer">{{ footer }}</div>
</div>
{% endmacro %}
Macros for SVG Icons
{% macro icon(name, size) %}
{% set size = size|default(24) %}
{% if name == 'search' %}
<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
{% elseif name == 'menu' %}
<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
{% endif %}
{% endmacro %}
Macro vs Include vs Embed
| Approach | Best For |
|---|---|
| Macro | Reusable functions with parameters (buttons, icons, cards) |
| Include | Reusable template chunks with current context (header, footer) |
| Embed | Customizable template sections with overridable blocks |
Common Mistakes
Forgetting to import macros: Every template that uses macros from another file must have an
{% import %}or{% from %}statement at the top. Without it, the macro call fails silently or throws an error.Using
_selffor imported macros:_selfonly works for macros defined in the same template. For imported macros, use the alias:{{ forms.input() }}not{{ _self.input() }}.Assuming macros have context access: Macros do not have access to the template's variables by default. Pass all needed values as parameters.
Over-engineering with macros: Simple one-line or single-use fragments do not need to be macros. Use macros when the same pattern appears in 3+ places.
Circular macro references: Recursive macros must have a base case to prevent infinite loops. Always include a condition that stops Recursion.
Practice Questions
What is the syntax to define a Twig macro named "alert"? Answer:
{% macro alert(message, type) %} ... {% endmacro %}. The macro can then be called with{{ _self.alert('Warning!', 'error') }}.How do you import macros from an external file? Answer: Use
{% import 'macros/forms.twig' as forms %}for all macros, or{% from 'macros/utils.twig' import icon %}for specific macros.Why might a macro not have access to a template variable? Answer: Macros have their own scope and do not inherit the template context. Variables must be passed explicitly as parameters.
What is the difference between a macro and an include? Answer: A macro is a parameterized function that returns HTML. An include renders another template file with access to the current context. Macros are for reusable components; includes are for reusable template sections.
Challenge: Create a complete macro library for a documentation site that includes: an icon macro that renders SVG icons (search, menu, close, arrow, check, warning), a button macro with style (primary, secondary, outline, ghost) and size (sm, md, lg) parameters, a badge macro for status labels (new, updated, deprecated, beta), a tooltip macro that shows help text on hover, a breadcrumb macro that renders hierarchical navigation from a page path, and a pagination macro with page numbers. Import and use all macros in at least three different templates.
FAQ
{{< faq "Can I pass a template as a parameter to a macro?" "Yes, pass the rendered template string as a parameter. The macro outputs it directly: {% macro wrapper(content) %}<div class=\"wrapper\">{{ content }}</div>{% endmacro %}." >}}
{{< faq "What happens if I import a macro file that doesn't exist?" "Grav throws a Twig error: Unable to find template \"macros/nonexistent.twig\". The file path must exist relative to the templates directory." >}}
Mini Project
Goal: Build a reusable macro library for a Grav site and use it across multiple templates.
- Create
templates/macros/directory with these macro files:cards.twig: card, card-grid, card-featurednavigation.twig: breadcrumb, pagination, menu-treemedia.twig: image, video-embed, galleryui.twig: button, badge, icon, alert, tooltiptypography.twig: heading, code-block, blockquote, list
- Each macro must have parameter defaults and handle edge cases (empty values, missing parameters)
- Create a blog listing template that imports and uses card macros
- Create a documentation template that uses navigation macros
- Create a landing page that uses media and UI macros
- Create an icon-only version for each SVG-based macro
- Ensure all macros work when imported with both
{% import %}and{% from %} - Test macros in templates from both parent and child themes
What's Next
Now you can create reusable macro components. Next, learn Twig inheritance patterns:
Continue to Lesson 17: Twig Inheritance — Multiple-level extends, blocks, embeds, and template hierarchy.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro