Creating templates

We talked about the general architecture in the last part. Now we are focusing on what is or should be inside the templates themselves and how to use the various features Twig ships with.

  1. How to make extends, embeds and uses work?
  2. What’s so special with components and how to use them?
  3. How to use the various template features like insert tag rendering, images or translations?
  4. How should I name my templates?

Reusing template parts

Twig supports a lot of different ways how to reuse Template parts and Contao enhances nearly all of them as well with the managed namespace concept: Extends, includes, embeds, horizontal reuse and macros.


A template can extend another one, called the parent or more general the base template. When extending a template, your template cannot contain content outside blocks, anymore, but you can adjust blocks of the parent template.

  • Typically, you want to replace and extend at the same time. This means, you name your template like the parent to make the loader choose yours when rendering, and then you reuse as much as possible from the parent by extending it.

  • This isn’t mandatory, though — variant templates are a good example for when you would want to create a new template but still only adjust things from the original one.

  • You can also use base templates like abstract base classes if you want to share a basic markup (implementation) across multiple children. The content_element/_base.html.twig is an example for this. Although this template does not contain a lot of markup, having it adds another handy use-case: If you want to adjust all content elements, e.g. by adding something to the wrapper, you now only need to adjust a single file.

Read more about the original implementation of the extends tag in the official Twig documentation.


Extend another template

The original template @Contao/product.html.twig that displays a (fictive) product entity:

{# Outputs a product with title and some details. #}
<section class="product">
    {% block title %}<h1>{{ product.title }}</h1>{% endblock %}
    {% block details %}
        <ul class="product--details">
            {% for detail in product.details %}
                <li>{{ detail }}</li>
            {% endfor %}
    {% endblock %}

We want to enhance the product title with an image:

{% extends "@Contao/product.html.twig" %}

{% block title %}
    {{ parent() }}
    <img scr="{{ product.imageSrc }}" alt="{{ product.title }}" />
{% endblock %}

With some demo data, you would then get the following HTML:

<section class="product">
    <h1>Fair-trade Coffee</h1>
    <img src="assets/images/coffee_package.jpg" title="Fair-trade Coffee"/>
    <ul class="product--details">
        <li>Nice aroma</li>
        <li>Lots of caffeine</li>

With the {{ parent() }} function you can insert the content of the current block as it was defined in the parent template.

Best Practice

Unless you are replacing a block with something completely different, always try to include the parent’s content. This way multiple extensions can contribute things to the same block, and you automatically profit from their improvements and additions as well.


If you want to include a whole template in another one, you can either use the {% include %} tag or the include() function. While the tag looks analogous to when embedding templates, the function returns the template as a string, which might come handy in rare cases, where you need to further process the result before outputting. Which one you choose is a matter of taste.

Read more about the original implementation of the include tag or the include function in the official Twig documentation.


Include a template partial

Assume you want to have a reusable call-to-action button. We create a simple @Contao/_action_button.html.twig template — we can enhance it later on without having to touch the places we are using it:

{% block button_container %}
    <div class="call-to-action">    
            {% block button_inside %}
                {{ title|default('Click me!') }}
            {% endblock %}
{% endblock %}

Now we can include it anywhere:

<div class="advertisement">
        Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
        eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
        voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
    {% include "@Contao/_action_button.html.twig" with {title: 'Buy this poetry on a shirt!'} %}
Best Practice

Templates, that are only used by or inside others, but never directly rendered, are called partial templates or just partials. To make their usage directly obvious, prefix the filename with an underscore (_) like we did in the above example.


Sometimes you want to include a template, but at the same time adjust some of its blocks. For this, you can embed the template.

Read more about the original implementation of the embed tag in the official Twig documentation.


Embed a template partial

We continue the example from the includes section. But now, when inserting the button, we want to add an icon in front of the title. We overwrite the button_inside block of the embedded template just for this usage:

<div class="advertisement">
        Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
        eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
        voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
    {% embed "@Contao/_action_button.html.twig" with {title: 'Buy this poetry on a shirt!'} %}
        {% block button_inside %}
            <img src="files/poetry.svg" alt="Poetry"> {{ parent() }}
        {% endblock %}
    {% endembed %}

Horizontal reuse

Similar to what traits are for classes in PHP, in Twig the {% use %} tag allows to reuse blocks of other templates without extending them. Used blocks are not output by default, but instead made available to the block() function.

While includes and embeds are very powerful, there is a hidden pitfall when using them to build reusable templates. Whenever you use these functions, Twig creates a new isolated scope in the template, that won’t be accessible from outside. So, if a template A extends from a template B, that includes/embeds another template C, you won’t be able to access or adjust blocks of C inside A. Here, horizontal reuse comes to the rescue.

Horizontal reuse is one of the most powerful parts of Twig. At the same time it’s the most complex one. So let’s dissect the various parts needed for it to work.

  • The block() function renders a block, that is available to the current template. Available means: defined in the same file, any parent template or manually imported by a {% use %} statement (see below).

    {% block foo %}Foo {{ bar }}{% endblock %}
    {# Output the "foo" block again #}
    {{ block('foo') }}
  • When using block(), the function renders the given block with the current template parameters. To render it in a different context, you can surround it with the {% with %} … {% endwith %} block tag, which allows defining template parameters, that are scoped to just this block. By default, the defined parameters are merged with the current ones, to suppress merging, you can add the only keyword.

    {# Output the "foo" block with the "bar" parameter being set to "baz" #}
    {% with {bar: 'baz'} only %}
      {{ block('foo') }}
    {% endwith %}
  • Finally, the {% use %} statement imports all blocks of the given template without outputting them. They will
    then be available to be output using a combination of {% with %} and the block() function. When importing, it could potentially happen, that there are blocks with identical names. To work around this, you can rename the blocks while importing.

    {% use "@Contao/_foo.html.twig" %}
    {% use "@Contao/_cat.html.twig" with sound as meow  %}
    {% use "@Contao/_dog.html.twig" with sound as bark %}
    {# Output the "sound" block of the cat template, that was imported as "meow" #}
    {{ block('meow') }}

Read more about the original implementation of the use tag, the with tag and the block function the in the official Twig documentation.


Make blocks available with horizontal reuse

We again build on top of the example of a button partial from the includes section. But this time, we make it into an abstract reusable @Contao/_advertisement.html.twig template:

{% use "@Contao/_action_button.html.twig" %}

<div class="advertisement">
    {% block description %}{% block %}
    {{ block('button_container') }}

Now, let’s actually use it in our @Contao/poetry_advertisement.html.twig. We are extending the just created template and adjust its own and its used blocks to get the same result as in the embed example:

{% extends "@Contao/_advertisement.html.twig" %}

{% block description %}
        Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
        eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
        voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
{% endblock %}

{% block button_inside %}
    <img src="files/poetry.svg" alt="Poetry"> {{ parent() }}
{% endblock %}

Did you note, how we were also able to adjust the button_inside block, here? We can either adjust this block in the original @Contao/_action_button.html.twig template (to affect all action buttons), in the @Contao/_advertisement.html.twig (to affect all buttons in advertisements) or, like we did here, in the @Contao/poetry_advertisement.html.twig (to only effect this special advertisement). Neat!

Best Practice

In Contao we are using the term component when we mean a piece of reusable template logic, that is intended to be imported with the {% use %} statement. In order for it to be output in the block() function, we make sure our whole component code is wrapped into a single block. As a convention, we call the block <name>_component. We also put each component in its own file inside the component directory.

Example: The figure component lives in the @Contao/component/_figure.html.twig file and is wrapped in a figure_component block. Read more about this in the section about Contao components.


Twig also features macros via the {% macro %} tag. These are very similar to how PHP functions work and also allow to reuse template logic. Head over to the Twig docs, to read more about how the macro tag works in detail.


Using macros to render a recursive structure

Macros behave like functions, they are well suited if you want to encapsulate a certain logic. Here we want to render a recursive tree structure (like a menu). Each node can contain more child nodes, and only the leafs contain values:

{% macro tree(node) %}
        {% for child_node in node.children %}
                {% if child_node.children|length %}
                    {{ _self.tree(child_node) }}
                {% else %}
                    {{ child_node.value }}
                {% endif %}
        {% endfor %}
{% endmacro %}

<div class="tree">
    {{ _self.tree_node(tree) }}

Do not overuse macros, especially in extensions. They can help avoid duplication in your code, but they are way less adjustable for others. Many things can also be done by using blocks and the block() function instead.

Contao components

By understanding how components work in Contao, you can get the most out of the core templates. You also might want to reuse them in your own code. Please familiarize yourself with the concept of horizontal reuse as we are heavily going to make use of it.

Let’s look at a real world example first. In Contao, a template component could look like this (taken from @Contao/component/_figure.html.twig, a bit simplified):

{% use "@Contao/component/_picture.html.twig" %}

{% block figure_component %}
    {% set figure_attributes = attrs()
    <figure{{ figure_attributes }}>
        {% if not figure.linkHref|default %}
            {# Media #}
            {% block media %}
                {{ block('picture_component') }}
            {% endblock %}
        {% else %}
            {# Media wrapped with link #}
            {% block media_link %}
                {% set link_attributes = attrs(figure.linkAttributes(true)|default)
                    .set('title', figure.metadata.title)
                <a{{ link_attributes }}>{{ block('media') }}</a>
            {% endblock %}
        {% endif %}

        {# Caption #}
        {% block caption %}
            {% if figure.metadata and figure.metadata.caption %}
                {% set caption_attributes = attrs()
                <figcaption{{ caption_attributes }}>
                    {%- block caption_inner -%}
                        {{- figure.metadata.caption|raw -}}
                    {%- endblock -%}
            {% endif %}
        {% endblock %}
{% endblock %}

Wow, there is a lot going on. For a moment, let’s remove the HTML attributes and strip the example further down.

It is no easier to see the <figure> HTML tag with a <figcaption> and some media, optionally wrapped in an a tag. Everything is placed in a block called figure_component.

{% block figure_component %}
        {# Media, optionally wrapped in a link #}
        {% if not figure.linkHref|default %}
            {% block media %}
                … here would be some media, like a picture …
            {% endblock %}
        {% else %}
            <a>{{ block('media') }}</a>
        {% endif %}

        {# Caption #}
            … here goes the caption …
{% endblock %}

Now, try going back to the original version. You’ll find, that the “media” bit isn’t implemented in the template, but instead uses and renders another component, the picture component (yes we’ve got template reuse everywhere). You also see the “set and merge” pattern being heavily in use (more on that further down). And we’ve got a lot of blocks. All this makes the template look more complex, but extremely adjustable on the other hand.

Don’t worry — your templates do not have to feature the same level of adjustability. Consider it, when creating an extension that ships components, though. For the Contao core, we try to value adjustability a bit higher in general, as these templates are likely to be handled by a lot of parties.

Adjusting components

Talking about adjusting: You saw in the examples earlier in this article how you would adjust blocks introduced by components. But how would you adjust a component itself (globally)?

You cannot extend components! In general: Twig does not allow to extend from templates that are being “used” at the same time. Use and overwrite the component’s blocks instead (see example below).


Globally adjusting the picture component

Let’s assume you want to add a custom class to every <img> tag and also wrap it in a div container. Luckily, this can be done with minimal effort — you would overwrite the @Contao/component/_picture.html.twig template, import the original templates’ blocks, and then make the needed adjustments:

{% use "@Contao/component/_picture.html.twig" %}

{% block image %}
    {% set img_attributes = attrs(img_attributes|default).addClass('my-image') %}
    <div>{{ parent() }}</div>
{% endblock %}

Although, we are overwriting a block without extending a template, we can use the parent() function. It works as expected outputting the original block’s content. The rest of our adjustments is basic HtmlAttributes behavior — we set a variable, we know the parent block is going to merge and use, and we’re done.

Grouped parameters

Until now, we only looked at the figure and picture components. They are special in a way, that every data they output, comes from a single Figure object (note the figure template parameter being used everywhere). Most of the time, you want to use several parameters, though.

If you dare, take a look at the list component, another very powerful, yet complex component: source code. Don’t worry about the javascript magic to sort and randomize lists at the moment but instead focus at the very beginning. Here is a stripped down version:

{% block list_component %}
    {% set list = list|default(_context) %}
    <{{ tag_name }}>
    </{{ tag_name }}>
{% endblock %}

The component begins with a variable declaration for a variable called list (not by accident: like the component), that, if not already existing, will be set to the current context using the special _context variable. Inside the component, everytime a parameter is used, the list version is used: list.items, list.randomize_order, and so on.

This effectively allows passing parameters individually or grouped under a Twig object. But why? When outputting multiple components, you can neatly group the data (already in the controller). By using the default names (here rendering with parameters: ['headline' => […], 'list' => […]]), you can omit any mapping needed with a {% with %} block. More importantly, the component’s parameters will never collide if named the same.

{# Output a headline and a list #} 
{{ block('headline_component') }}
{{ block('list_component') }}

On the other hand, manually outputting blocks is less cumbersome without the additional group:

{% with {tag_name: 'h2', headline: 'My headline' } %}
    {{ block('headline_component') }}
{% endwith %}
Best Practice

When creating components, allow grouping their parameters under a single object named like the component. For this add a {% set <name> = <name>|default(_context) %} variable definition to the beginning of your component and then only use the defined variable further down. Bonus points are given, if you add a little doc block to your file, listing the available parameters and how to use them (look at the core’s components for some inspiration).

Template features

The Contao Twig extension does most of the setup needed to provide our custom functionality, like the managed namespace, the input encoding tolerant escapers, the legacy interoperability, or several filters and functions to make your life easier. On top of that, many Twig features just work out of the box for us.

Insert tags

Make sure you read and understood the section about encoding before continuing!

If you want to output a string, that contains insert tags, you, as the template designer, need to decide if and how it should get treated. In case insert tags should get replaced, you can precisely control what to encode and what to keep raw (see table below): Everything, all but the result of evaluating the insert tags, or nothing at all. This way you can keep the security risk low while still allowing features.

The examples in the following table use the context ['text' => 'This is some <b>very</b> {{br}} rich text.] that both includes some user input HTML and the {{br}} insert tag (which evaluates to <br> if being replaced). The given output examples were generated using the escaping strategy for HTML.

Handling Usage Example output in a HTML context
plain text
treat insert tags as text,
encode everything
{{ text }} This is some &lt;b&gt;very&lt;/b&gt; {{br}} rich text.
encode everything
{{ text|insert_tag }} This is some &lt;b&gt;very&lt;/b&gt; &lt;br&gt; rich text.
replace and trust insert tags
⚠️ only encode the text around the insert tags
{{ text|insert_tag_raw }} This is some &lt;b&gt;very&lt;/b&gt; <br> rich text.
raw text
treat insert tags as text,
⚠️ encode nothing
{{ text|raw }} This is some<b>very</b> {{br}} rich text.
replace and trust everything
⚠️ encode nothing
{{ text|insert_tag|raw }} This is some <b>very</b> <br> rich text.

Think about your scenario, before deciding which option to use. Should insert tags even be replaced? If so, they might for instance output HTML. Do you trust this? And, sticking to the HTML example, should a user be able to write HTML? With |insert_tag_raw you can for example allow an editor to use the {{br}} insert tag while still disallowing the use of any custom HTML around it.

Best Practice

There is also an insert_tag() function to directly render an insert tag, but you should avoid using it if possible. Use proper template functions and filters instead. Keep in mind, that insert tags are meant to be used by editors inside back end and not to structure content in templates.

Response context

Sometimes a template only includes part of a page, like a content element, but still wants to contribute content that is global to the page.

since 5.0 For this, you can use the add tag in Contao. It allows adding content to the end of the document head or body. If you want the block to be output only once (no matter how often the template is rendered on the current page), you can provide a name. This is especially helpful when adding a generic javascript code or styles, that do not make sense getting inserted multiple times.

{# Add code to the end of the document body #}
{% add to body %}{% endadd %}

{# Add code to the end of the document head #}
{% add to head %}{% endadd %}

{# Add code to the end of the document body once #}
{% add "foo" to body %}{% endadd %}

{# Add code to the end of the document head once #}
{% add "bar" to head %}{% endadd %}

If you want to add JSON-LD metadata to the current page, you can use the add_schema_org function. It expects an array of data and returns nothing. For this, you will typically want to wrap a call to it in Twig’s do tag:

{% do add_schema_org() %}

Behind the scenes, these features build on top of the response context concept. It is the responsibility of the page template (like fe_page) to ultimately output the gathered data.

HTML Attributes

since 5.0 HTML attributes are a heavily used feature in HTML. In the context of templates, they are also one of the primary things, that people want to adjust. This is why we created the HtmlAttributes class together with the attrs() function to create an instance of said class.

The HtmlAttributes class features a lot of helper methods in a fluent API style. With it, you can easily parse, merge and compose attributes on the fly. Anything you pass to the attrs() function will be passed to the classes' constructor: This could either be an associative array of attributes, a string of attributes that will get parsed or another HTMLAttributes object that will get merged.


Using the HtmlAttributes class in Twig (Part 1)

You can transpose the following…

<div id="main" class="{{ classes|join(' ') }}"{% if tooltip|default %} data-tooltip="{{ tooltip }}{% endif %}">
    {{ text }}

into an equivalent chain of calls:

<div{{ attrs().set('id', 'main').addClass(classes).setIfExists('data-tooltip', tooltip|default) }}>
    {{ text }}

Using {{ }}, you can directly output the class. It implements a __toString() method, already encodes the output and is registered as a so-called “safe class” in our Twig extension, that the escaper filters are skipped on.

We are heavily using a pattern, that we call “set and merge”, in the core’s templates. Here, instead of outputting the attributes directly, a variable is assigned first and the same variable (falling back to an empty string) is used when constructing the attributes, effectively merging any previously set data. With this change, extenders of the template, that only want to adjust attributes, do not have to overwrite anything. As their code gets executed first, they can simply create the variable with the same pattern, and it will then get merged by the parent.


Making HtmlAttributes better adjustable (Part 2)

Let’s use the “set and merge” pattern:

{% set text_attributes = attrs(text_attributes|default)
    .set('id', 'main')
    .setIfExists('data-tooltip', tooltip|default)
<div{{ text_attributes }}>
    {{ text }}

The magic really shows, when we want to adjust this template. For instance to add a custom class:

{% extends "@Contao/text_example.html.twig" %}

{% set text_attributes = attrs(text_attributes|default).addClass('custom-class') %}

We could have omitted the constructor argument in the last example. If you cannot be sure, that the template you are editing is always the first one to be executed (for instance in an extension), using the “set and merge” pattern again is the way to go. This way it also works with multiple extenders that want to change things.

Please refer to the doc block comments on each of the fluent interface methods of the HtmlAttributes class for details on how to use it.


since 5.0 The figure and picture components are suited to render any built Figure object. In case you cannot or don’t want to create a Figure in the controller, you can alternatively use the figure function to build a Figure instance on the fly. Internally, this uses the FigureBuilder from the Contao image studio. In case you also want to create a picture/resize configuration on the fly, you can use the respective picture_configuration function.

{# It's enough to specifiy the resource and size… #}
{% set figure = figure('path/to/my/image.png', '_my_size') %}

{# …but you can also go wild with the options: #}
{% set figure = figure(id, [200, 200, 'proportional'], { 
  metadata: { alt: 'Contao Logo', caption: 'Look at this CMS!' },
  enableLightbox: true,
  lightboxGroupIdentifier: 'logos',
  lightboxSize: '_big_size',
  linkHref: '',
  options: { attr: { class: 'logo-container' } }
}) %}

{# You can even use a custom - on the fly - picture configuration: #} 
{% set special_size = picture_config({
    width: 400,
    height: 400,
    resizeMode: 'proportional',
    sizes: '0.75,1,1.5,2',
    items: [{
        width: 200,
        height: 100,
        media: '(max-width: 140px)',
}) %}
{% set figure = figure(uuid, special_size) %}

You can then query the object for things like metadata or sizes and/or output it as an image, for example using the pre-made _figure component:

{% use "@Contao/component/_figure.html.twig" %}

{# Create the figure inline or use the one already in our context #}
{% set my_figure = figure() %}

{# Output it using the figure component #}
{% with {figure: my_figure} %}{{ block('figure_component') }}{% endwith %}

In Contao 4.13 you need to use the contao_figure instead of the figure function. While allowing the same arguments, this function will directly render the (default or given) image template instead of returning a Figure object. Note, that this function is deprecated since Contao 5.0.


since 5.0 You can use the highlight and highlight_auto filters to generate code highlighting with the scrivo/highlight.php library. You can either pass a language to use or let the library guess the language (highlight_auto). In the latter case, you can optionally constrain the selection to a given subset of languages.

{# Output code directly as a string… #}
<pre><code>{{ code|highlight(language) }}</code></pre>

{# … and/or query the result properties: #}
{% set highlighted = code|highlight_auto %}
The detected language was {{ highlighted.language }} with a relevance of {{ highlighted.relevance }}.

since 5.0 The format_bytes filter transforms a number representing a file size in bytes into a human-readable form. It therefore uses the MSC.decimalSeparator, MSC.thousandsSeparator and UNITS declarations from the global translation catalogue.

{# outputs 1536 #} 
{{ size }}

{# outputs "1.5 KiB" #}
{{ size|format_bytes }}

{# outputs "1.500 KiB" #}
{{ size|format_bytes(3) }}


The Symfony Twig bridge includes a trans tag and function to handle and output translations. If you want to define the default translation domain, that is used in the whole template, you can use the trans_default_domain tag. Read more about this in the Symfony Twig documentation.

{% trans_default_domain "contao_default" %}

    {# Output a translation using the default domain: #}
    <a href="more.html">{{ 'MSC.readMore'|trans }}</a>

    {# Output a translation containing placeholders and a custom domain. You
       can either use an array with unnamed parameters, or an object, that
       explicitly defines replacements: #}
    {{ 'tl_race.result'|trans([best_time], 'contao_race') }}
    {{ 'tl_race.result'|trans({'%best%': best_time}, 'contao_race') }}

If you need a translation in a certain locale, this is also possible by passing the locale as an argument to the function. For example trans([], 'contao_default', 'de') or trans(locale='it').


To generate URLs from within Twig templates, you can use the path() function, that is shipped with the Symfony Twig bridge. Read more about this function in the Symfony Twig documentation.

<a href="{{ path('my_custom_route', {id: 42}) }}">Watch for this!</a>

As of writing this, there is unfortunately no replacement for PageModel::getFrontendUrl(), yet. If you need this, for now, stick to generating the URLs in your controller and then pass them to your template (where they are only output).

since 5.0 The prefix_url filter prefixes relative URLs with the request base path. This is needed when dealing with relative URLs on a page that does not set a <base> tag.

{# will outpuot something like "" #}
{{ 'some/relative/url.html'|prefix_url }}

Render Content Elements

This feature is available in Contao 5.2 and later.

You can render content elements directly in your Twig templates via the content_element() function. This function either takes a database ID of an existing content element, a ContentElementReference object - or just the type of a content element. In the latter case you can then also define all the options with which this content element should be rendered. This allows you to render content elements on the fly without having to define them in the database.

{# Render a specific content element from the database #}
{# This is similar to the `insert_content` insert tag #}
{{ content_element(15) }}
{# Render a `gallery` content element on the fly #}
    content_element('gallery', {
        multiSRC: data.myGallery,
        sortBy: 'random',
        perRow: 4,
        limit: 12,
        fullsize: true

Render Front End Modules

This feature is available in Contao 5.2 and later.

You can render front end modules directly in your Twig templates via the frontend_module() function. This function either takes a database ID of an existing front end module - or just the type of a front end module. In the latter case you can then also define all the options with which this front end module should be rendered. This allows you to render front end modules on the fly without having to define them in the database.

{# Render a specific front end module from the database #}
{# This is similar to the `insert_module` insert tag #}
{{ frontend_module(42) }}
{# Render a `navigation` front end module on the fly #}
    frontend_module('navigation', {
        levelOffset: 1,
        navigationTpl: 'nav_custom',

Naming convention

Because of the shared @Contao namespace, it is important, that the Contao ecosystem tries to stick to a common naming convention. For the sake of readability, we are omitting the @Contao/ prefix to the logical name in the following examples.

Best Practice

Here are some general rules to consider for directory as well as template file names:

  • Use snake_case, such as content_element or foo_bar_baz.
  • Only use lowercase characters and avoid special characters. Don’t use dots except for the file extension.
  • Always include the file type and .twig as a file extension, e.g. cat.svg.twig or slider.html.twig.
  • The name already includes the directory/subdirectory, never repeat any directory name in the file name (content_element/text/text_highlight.html.twigcontent_element/text/highlight.html.twig).

Directory structure

Twig allows the usage of directories as part of the template name. For extensions or if you want to use the application’s main template directory, you might want to read the part about how to properly set up the Twig root first.

Best Practice

We strongly suggest, that you organize your templates like outlined below. Especially extensions should stick to these conventions, so that users do not have to mix different structures in their application’s template directory!

Put your templates in one of the following directories in your Twig root. Only create further subdirectories inside these directories, if there is a justified need for it. You might want to pitch your idea on GitHub first, so that we can potentially make it available here for others to use as well.

Category Directory Template example (@Contao/…)
Reusable components component backend/module_wildcard.html.twig
Content elements content_element content_element/gallery.html.twig
Frontend modules frontend_module frontend_module/feed_reader.html.twig
Anything related to the back end
Until we further refactor the back end, this directory is reserved for the Contao core.
backend @Contao/backend/module_wildcard.html.twig
Forward compatibility
This directory is reserved for extensions, that need to provide templates in order to be forward compatible. You should never extend from these templates, nor render them directly.
compat compat/content_element/code.html.twig
Extension-specific things <extension>*) notification_center/mail.html.twig

*) Only put things in an extension-specific directory, if it won’t fit in any other category. If your extension includes a lot of templates, it is fine to create a subdirectory <extension> (e.g. several content element templates for the foo extension: content_element/foo/…). Do not use a vendor prefix if your extension has a unique name (e.g. notification_center) or only makes sense to have one of a kind to be installed at the same time (e.g. opengraph for an extension that adds opengraph tags). The same holds true for the template file names themselves.


In Contao, you can create templates in multiple variants and let back end users pick the one they need. Read more about the architecture and how they are used in the template variants section.

Best Practice

Variant templates should be put in a directory named like the template’s file name excluding the file extension.

Example: A variant to the content_element/gallery.html.twig template, should be placed in the content_element/gallery directory. The file name should describe what the variant is about, e.g.: content_element/gallery/carousel.html.twig if images get displayed in a carousel instead.

since 5.2 If you want to change the display names of the templates, that are selectable in the back end dropdowns for content elements or frontend modules, you can create translations for them in the templates translation domain. The translation ID is equal to the template identifier:

# translations/templates.en.yaml
content_element/text: 'Default text'
content_element/text/special: 'Special text'

Partials and components

Partials are files that will never be rendered directly but included/embedded/used or extended from only. There is no difference in how they work, but the convention to distinguish them allows to easier find the main controller templates.

Best Practice

Include a leading underscore to a partial’s name. For instance: content_element/_base.html.twig. The same applies for component files, such as component/_list.html.twig. Follow the rules about directories to decide where to put your partial. It’s fine, and also quite typical, that they coexist in the same directory as the files that are using them.