Skip to content

Twig Include and Render Functions

Include

The include statement in twig renders the content of a twig file inside of the one where include is being used. This is very useful for making reusable components or partials.

For example we often use include the social media partial in the footer.twig layout for our sites. This is already included by default in our boilerplate.

<!-- footer.twig -->
    {{ include('/partials/socials.twig') }}
<!-- socials.twig -->
<div class="flex justify-center gap-5 p-0 py-4 mx-auto lg:justify-start">
    {% set socials = [ 
        {'name': 'facebook', 'svg': 'facebook.svg.twig', 'class': ''}, 
        {'name': 'instagram', 'svg': 'instagram.svg.twig', 'class': ''}, 
        {'name': 'youtube', 'svg': 'youtube.svg.twig', 'class': ''}, 
        {'name': 'linkedin', 'svg': 'linkedin.svg.twig', 'class': ''}, 
        {'name': 'x', 'svg': 'x-twitter.svg.twig', 'class': ''} 
    ] %} 

    {% for social in socials %} 
        {% if options[social.name] %}
            <a
                class="flex items-center group"
                href="{{ options[social.name] }}"
                target="_blank"
                aria-label="(opens in a new window)">
                {% include "svg/socials/" ~ social.svg with { 'class': social.class ~ ' fill-white w-10
                transition-transform group-hover:scale-110' } %}
                <span class="sr-only">{{social.name}}</span>
            </a>
        {% endif %} 
    {% endfor %}
</div>

By default the included file will have access to the same context as the file it's being included in. Howver, in this case, socials.twig is able to access options['facebook'] because options is added to Base context which is available globally.

As you can see in the example above where the svg file is being included, you can insert data to be passed to the template file you are including as well. In a case like this you might not want to add the same classes to every social media svg. Instead, we have included the file with the class key set to social.class which it is grabbing from the array of objects above (in this case none of them have CSS that needs to be specific to them so all of their social.class keys are set to empty strings) concatenated with a list of Tailwind CSS classes that can be passed to all of the svg twig files. Each svg twig file then can invoke {{ class }} to retrieve those classes from data and apply them in the appropriate places. This makes it easier to edit all of the twig files that are being looped over in one place rather than going to each individual twig file.

<!-- facebook.svg.twig -->
<svg class="{{class}}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
    <!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
    <path
        d="M504 256C504 119 393 8 256 8S8 119 8 256c0 123.78 90.69 226.38 209.25 245V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.28c-30.8 0-40.41 19.12-40.41 38.73V256h68.78l-11 71.69h-57.78V501C413.31 482.38 504 379.78 504 256z" />
</svg>

It also makes it easier to use a component in different locations across the site, with the option to pass data that makes it style or behave differently depending on what you pass in as data. For example, in the example below the generic footer-navigation.twig component is being used to render both the footer navigation menu and the policies menu however they are being styled differently and less generically through passing in different data that represent classes in particular places within the footer-navigation.twig file (see the different gap sizes in the 'menu_classes'). The 'dividers' key being passed as data determines whether a '|' pipe will appear between menu items and is turned on and off functionally with a boolean and is checked with a conditional inside of footer-navigation.twig.

<!-- footer.twig -->

    <!-- footer navigation menu -->
    {% set footer_nav = [menus.footer_nav1, menus.footer_nav2, menus.footer_nav3] %}

    <div class="py-[70px] lg:py-20 mx-auto max-w-content grid grid-cols-1 lg:grid-cols-3 gap-x-[30px] gap-y-[42px] grid-flow-row max-lg:w-fit ">
        {% for menu_column in footer_nav %}
            <div class="flex flex-col items-center lg:items-start">
                <nav class="w-full text-2xl">

                    {% include '/components/footer-navigation.twig' with { 'footer_menu_items':menu_column.items, 'menu_classes': 'flex flex-col gap-[42px]', 'menu_item_classes': '', 'anchor_classes': 'hover:text-red-500 link-underline-red hover:link-underline-red--hover', 'dividers': false } %}

                </nav>
            </div>
        {% endfor %}
    </div>



    <!-- Policies Menu -->
    <div class="flex flex-wrap justify-center flex-shrink-0 gap-x-3 gap-y-1 lg:flex-nowrap">

        {% include '/components/footer-navigation.twig' with { 'footer_menu_items' : menus.policies_menu.items, 'menu_classes': 'flex gap-x-3 gap-y-1 flex-wrap sm:flex-nowrap justify-center', 'menu_item_classes': 'flex-shrink-0', 'anchor_classes': 'hover:text-red-500 link-underline-red hover:link-underline-red--hover', 'dividers' : true } %}

    </div>
<!-- footer-navigation.twig -->
<menu class="{{ menu_classes }}">
    {% for item in footer_menu_items %}
        <li class="{{ menu_item_classes }}">
            <a href="{{ item.url }}" class="{{ item.class }} {{ anchor_classes }}" title="{{item.name}}" aria-label="{{item.name}}">
                    {{ item.name }}
            </a>
        </li>
        {% if dividers %}
        <div>|</div>
        {% endif %}
    {% endfor %}
</menu>

Include Limitations

A file cannot be included inside of itself, for example if you are inside of header-navigation.twig and want to use the header-navigation.twig template recursively to render out child submenus, you cannot include header-navigation.twig. If you try you will recieve a critical error on your site due to an infinite loop.

In past versions of Origins we handled this by using macros.

<!-- header.twig -->
{% import "components/header-navigation.twig" as menu %}
//
//
//
<menu :class="{'hidden' : ! navIsOpen}" class="left-0 hidden w-full text-white bg-black lg:flex max-lg:pb-4 max-lg:flex-col max-lg:absolute has-global-padding lg:justify-end lg:items-center top-full lg:space-x-4">
    {{ menu.render_menu_items(header_menu.items, 1) }}
</menu>
<!-- header-navigation.twig -->

{% macro render_submenu_indicator(submenuIsOpen) %}
    <div x-on:click="submenuIsOpen = !submenuIsOpen" class="relative self-center w-4 h-4 cursor-pointer lg:hidden">
        <div :class="submenuIsOpen && 'rotate-90'" class="absolute transition-transform w-[2px] h-4 rounded-full bg-white left-0 right-0 mx-auto group-hover:bg-yellow"></div>
        <div :class="submenuIsOpen && '-rotate-90'" class="absolute transition-transform w-[2px] h-4 rounded-full bg-white rotate-90 left-0 right-0 mx-auto group-hover:bg-yellow"></div>
    </div>
{% endmacro %}

{% macro render_default_indicator() %}
    <div class="relative self-center w-4 h-4 cursor-pointer lg:hidden">
        <div class="absolute transition-transform w-[2px] h-4 rounded-full bg-white rotate-90 left-0 right-0 mx-auto group-hover:bg-yellow"></div>
    </div>
{% endmacro %}

{% macro render_menu_items(menu_items, depth) %}
    {% set text_size_map = {1: 'text-2xl', 2: 'text-xl', 3: 'text-lg'} %}

    {% for item in menu_items %}

        <li class="flex flex-col items-center list-none" x-data="{submenuIsOpen: false}">
            <div
                class="flex w-full">
                {# Menu Indicator #}
                {% if item.children %}
                    {{ _self.render_submenu_indicator(submenuIsOpen) }}
                {% else %}
                    {{ _self.render_default_indicator() }}
                {% endif %}

                {# Menu Item #}
                <a href="{{ item.url }}" class="{{ item.class }} {{ item.current ? 'text-green-300' }} list-none leading-8 px-4 {{text_size_map[depth]}}">{{ item.name }}</a>
            </div>

            {% if item.children %}
                <menu :class="{'hidden' : ! submenuIsOpen }" class="flex flex-col items-start justify-start hidden w-full pl-8 mt-2">
                    {{ _self.render_menu_items(item.children, depth + 1) }}
                </menu>
            {% endif %}
        </li>

    {% endfor %}
{% endmacro %}

Macros could be used to recursively loop through a menu with submenus. More recently, a render twig helper function was added that behaves and is formatted similarly to include but can be used recursively like macros.

Render

The render twig helper function is located in app/Twig/Functions.php. It takes a template and data, and then returns the template compiled with the data to render on the front end. So it is essentially the same format and takes the same parameters as include. The main difference between it and include is that it can be used in a template to render itself within that template.

public function render($template, $data)
    {
        return Timber::compile($template, $data);
    }

A place where this has been used in the default boiler plate is in the header.twig and header-navigation.twig templates.

<!-- header.twig -->
<menu :class="{'hidden' : ! navIsOpen}" class="left-0 hidden w-full text-white bg-black lg:px-16 lg:flex max-lg:pb-4 max-lg:flex-col max-lg:absolute has-global-padding lg:justify-end lg:items-center top-full lg:space-x-8">

        {{ render('/components/header-navigation.twig', {'items' : menus.header.items, li_classes: 'lg:py-4',}) }}

</menu>

When we call render we use {{}} instead of {% %}, but otherwise we similarly enter the path to the template file that we want to render, and pass the data we want to use within that template as an object.

<!-- header-navigation.twig -->
{% set
text_size_map = {0: 'text-2xl', 1: 'text-xl', 2: 'text-lg', 3: 'text-md'} %}

{% if items %}
    {% for item in items %}
        <li class="flex flex-col items-start justify-center list-none lg:items-center lg:h-full group {{li_classes}}" x-data="{submenuIsOpen: false}">
            <div class="flex flex-row items-center">
                {% if item.children %}
                    <div x-on:click="submenuIsOpen = !submenuIsOpen" class="relative flex-shrink-0 self-center w-[14px] h-auto cursor-pointer ">
                        <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 100 100" class="transition-transform duration-300 rotate-90 fill-red-500 lg:fill-white lg:group-hover:rotate-180 lg:group-hover:fill-red-500" :class="submenuIsOpen ? '!rotate-180 !fill-red-500' : 'rotate-90'" aria-label="open submenu">
                            <polygon points="50, 13.397 100, 100 0, 100"/>
                        </svg>
                    </div>
                {% endif %}
                <a x-on:click="navIsOpen = false" href="{{ item.url }}" class="{{ item.class }} list-none leading-8 px-[5px] {{text_size_map[item.level]}} {{ item.children ? '' : 'max-lg:pl-[20px]' }}" title="{{item.name}}" aria-label="{{item.name}}">{{ item.name }}</a>
            </div>
            {% if item.children %}
                    <menu x-cloak class="lg:border-gray-400 lg:border flex flex-col items-start justify-start hidden w-full lg:w-fit mt-5 lg:group-hover:flex gap-y-5 lg:bg-gray-100 lg:absolute  lg:pl-0 lg:gap-y-0 lg:justify-center lg:items-stretch lg:mt-0 lg:top-[112px] z-[98]" :class="{ 'hidden' : !submenuIsOpen }">

                        {{ render('/components/header-navigation.twig', {items: item.children, li_classes: 'lg:py-7 lg:px-16 text-center lg:hover:bg-gray-200 lg:border-b lg:border-gray-400 last:border-none text-black'}) }}

                    </menu>
            {% endif %}
        </li>
    {% endfor %}
{% endif %}

Within the header-navigation.twig above we also use render to render another copy header-navigation.twig inside of itself if there are children of a menu item. This allows us to handle the recursion of a menu without needing to use macros and with the option to send different data/classes in for styling depending on location.