Skip to content

Grav Twig Macros — Reusable Template Blocks and Imports

DodaTech Updated 2026-06-27 8 min read

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

  1. 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.

  2. Using _self for imported macros: _self only works for macros defined in the same template. For imported macros, use the alias: {{ forms.input() }} not {{ _self.input() }}.

  3. Assuming macros have context access: Macros do not have access to the template's variables by default. Pass all needed values as parameters.

  4. 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.

  5. Circular macro references: Recursive macros must have a base case to prevent infinite loops. Always include a condition that stops Recursion.

Practice Questions

  1. 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') }}.

  2. 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.

  3. 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.

  4. 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.

  5. 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

Can I use Twig blocks inside a macro?

No. Macros cannot contain Twig blocks ({% block %}). Blocks are for template inheritance. Macros are for reusable fragments. Use embed if you need both blocks and reusability.

How do I set default values for macro parameters?

Use the default filter: {% set size = size|default('md') %}. You can also use the ?: operator: {% set label = label ?: 'Default Label' %}.

{{< 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." >}}

Can macros be overridden by child themes?

Yes. If a child theme has a macros file in the same path, it overrides the parent theme's version. This is the same override mechanism as templates.

Mini Project

Goal: Build a reusable macro library for a Grav site and use it across multiple templates.

  1. Create templates/macros/ directory with these macro files:
    • cards.twig: card, card-grid, card-featured
    • navigation.twig: breadcrumb, pagination, menu-tree
    • media.twig: image, video-embed, gallery
    • ui.twig: button, badge, icon, alert, tooltip
    • typography.twig: heading, code-block, blockquote, list
  2. Each macro must have parameter defaults and handle edge cases (empty values, missing parameters)
  3. Create a blog listing template that imports and uses card macros
  4. Create a documentation template that uses navigation macros
  5. Create a landing page that uses media and UI macros
  6. Create an icon-only version for each SVG-based macro
  7. Ensure all macros work when imported with both {% import %} and {% from %}
  8. 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