Twig Support (Preview)

This feature is available in Contao 4.12 and later.

Do you want to try Contao with Twig templates? On this page you’ll find a quick start how to…

Please provide feedback on GitHub or the Contao Slack channel in case you notice bugs, unwanted behavior or have suggestions how to improve the experience. 🙏

Twig support is currently experimental and therefore not covered by Contao’s BC promise. Classes marked with @experimental should be considered internal for now. Although not likely, there could also be some behavioral changes, so be prepared.

Getting started with Twig

If you’re already familiar with Twig, you might want to skip this step.

Twig templates have their own syntax, but don’t be afraid, you’ll quickly find your way. Switch between the following tabs to see how an example PHP template would translate to an analog Twig template:

<div class="about-me">
  <h2><?= $this->name ?></h2>
  <p>I am <?= round($this->age) ?> years old.</p>

  <ul class="hobby-list">
    <?php foreach $this->hobbies as $hobby: ?>
      <li><?= ucfirst($hobby) ?></li>
    <?php endforeach; ?>
<div class="about-me">
  <h2>{{ name }}</h2>
  <p>I am {{ age|round }} years old.</p>

  <ul class="hobby-list">
    {% for hobby in hobbies %}
      <li>{{ hobby|capitalize }}</li>
    {% endfor %}

Learning the syntax

To output variables wrap their name in curly braces {{ foo }}, to use keywords like for wrap them in {% and %}, to further process any output, use filters |foo and functions bar().

Twig is very well documented - a good place to start is the Twig for template designers section that covers syntax details as well as helpful IDE plugins for autocompletion and syntax highlighting.

For quickly trying something out, you can use Twig fiddle - an online playground. Take a look at this demo fiddle for instance.

But why?

Twig is Symfony’s default way to write templates. It’s fast, safe and easily extensible. Contrary to PHP templates, Twig templates won’t contain business logic which allows to share them more easily between designers and programmers. This fact also helps you maintain a clean separation between your presentation and data/logic layer.

Twig also features a lot of powerful methods to structure your templates like including, embedding, inheriting, reusing blocks or macros, eases accessing objects with “property access”, has whitespace control, string interpolation features and a ton more… Give it a try!

Twig in Contao

Overwriting and extending

You can use Twig templates at any place you would use a Contao PHP template, just with a different file extension (.html.twig instead of .html5). It’s even possible to extend Contao PHP templates from within your Twig templates.

Go ahead and place a fe_page.html.twig in your template directory - this example will add a friendly headline above the main section and keep everything else the same:

{# /templates/fe_page.html.twig #}

{% extends '@Contao/fe_page' %}

{% block main %}
  <h1>Hello from Twig!</h1>
  {{ parent() }}
{% endblock %}
  1. Name your Twig templates like your Contao counterpart (except for the file extension) and put them in a regular Contao template directory. There can either be a Twig or a PHP variant of the same template in the same location.

  2. To extend an existing template (instead of completely replacing it) use the extends keyword and the special @Contao namespace (more about this, see below).

  3. Use the same block names as in the original template.

You cannot extend Twig templates from within PHP templates only the other way round.

Namespace magic

Twig templates live in namespaces like @Foo/my_template.html.twig (Foo) or @ContaoCore/Image/Studio/figure.html.twig (ContaoCore). We are automatically registering templates from the various Contao template directories in their respective namespaces:

Folder Namespace Prio.*)
Any bundle template/views directory. 1
@Contao_App Template directory of the application. 2
/templates @Contao_Global Global template directory. 3
Any theme directory. The path (foo/theme) will be transformed into a slug (foo_theme) and appended as a suffix. 4

*) Higher priority values mean “considered as template candidate first”.

You can run contao-console debug:contao-twig to get a list of all registered namespaces. If you want to list theme templates as well add the -t option with your theme path or slug. To filter for certain templates enter their name or prefix as an argument, e.g.: contao-console debug:contao-twig ce_text -t my/theme.

On top, we’re also providing a managed @Contao namespace which you should use whenever you do not know the exact namespace beforehand. This namespace will be substituted with a specific namespace when the templates are compiled. In each situation we’re choosing the next available template that has a lower priority than the current one.

And yes, you can totally use this to extend, embed or include templates. Have a look at the following example to get an idea.

Template hierarchy example

In this example, we’re dealing with four manifestations of the same card.html.twig template: two in bundles, two more in the application.

The original template of the card-bundle:

{# /vendor/foo/card-bundle/contao/templates/card.html.twig #}

{% import '@ContaoCore/Image/Studio/_macros.html.twig' as studio %}

<section class="card">
  {% block card %}
    <header class="title">
      {% block title %}{{ title }}{% endblock %}
      {% block content %}
        {{ studio.figure(figure) }}
        {{ description|raw }}
      {% endblock %}
      {% block footer %}<p class="author">by {{ author }}</p>{% endblock %}
  {% endblock %}

A card-time-bundle extending the original bundle and adding information to the footer - this bundle was loaded after the card-bundle, therefore it is further up in our template hierarchy:

{# /vendor/bar/card-time-bundle/contao/templates/card.html.twig #}

{% extends '@Contao/card' %}

{% block footer %}
  {{ parent() }}
  <p class="last-modified">edited at {{ modified_at|ago }}</p>
{% endblock %}

The card template of the global template folder adding some wrappers, because, you know, you can’t have enough divs.

{# /templates/card.html.twig #}

{% extends '@Contao/card' %}

{% block title %}<div class="inner">{{ parent() }}</div>{% endblock %}
{% block card %}<div class="inner">{{ parent() }}</div>{% endblock %}

And finally the application’s emoji theme adding, well, …

{# /templates/emoji/card.html.twig #}
{% extends '@Contao/card' %}

{% block title %}🤩 {{ parent() }} 🤯{% endblock %}

Resolving all extends in the right order would effectively yield the following template - note how each stage can adjust/contribute to blocks without the need to know about the others because every extend uses the managed @Contao namespace:

{% import '@ContaoCore/Image/Studio/_macros.html.twig' as studio %}

<section class="card">
  <div class="inner">
    <header class="title">
      🤩 <div class="inner">{{ title }}</div> 🤯
      {{ studio.figure(figure) }}
      {{ description|raw }}
     <p class="author">by {{ author }}</p>
     <p class="last-modified">edited at {{ modified_at|ago }}</p>

When extending, including or embedding templates from the @Contao namespace, the file extension is not considered. This means @Contao/card.html.twig will target the same template as @Contao/card.html5. For this reason you can omit the extension completely in that case.

Template context

If you are implementing your own modules or content elements (fragment controllers), you can follow Symfony’s way to do it. Create a Twig template instead of your usual PHP template (e.g. ce_my_content_element.html.twig) and render it from inside your controller:

 * @ContentElement(category="texts")
class MyContentElementController extends AbstractContentElementController
    protected function getResponse(Template $template, ContentModel $model, Request $request): Response
        return $this->render('@Contao/ce_my_content_element.html.twig', [
            'text' => $model->text,
            'my_variable' => 'foobar',

You have full control over the template context, i.e. the array passed as second argument. If you are rendering or replacing Contao templates, we are creating the context for you based on the template data of the original template.

⚠️ There is a small difference when it comes to callables like anonymous functions or closures. Every element in Twig’s context needs to be a literal or an object, so when overwriting a PHP Template, we’re wrapping any function in the template data into an object with __toString() functionality. In case you need to supply arguments, or you need anything else than string output, you’ll need to add .invoke() to the variable name:

// Template data, that was set for a PHP template 
    'normalValue' => 'foo',
    'lazyValue' => static function(): string {
        return 'foo';
    'fooFunction' => static function(string $value): string {
        return "foo-$value";
     'lazyArray' => static function(): array {
        return [1, 2, 3, 4, 5];
<?= $this->normalValue ?>
<?= $this->lazyValue ?>
<?= $this->fooFunction('bar') ?>
<?= implode(', ', $this->lazyArray) ?>
{{ normalValue }}
{{ lazyValue }}
{{ fooFunction.invoke('bar') }}
{{ lazyArray.invoke()|join(', ') }}


For historic reasons Contao uses input encoding, but Twig embraces the more sane output encoding. You can read more about the topic (and why you should favor output encoding) in this OWASP article about preventing Cross Site Scripting (XSS) attacks.

Why you should care

The gist: You, as a template designer, have to decide how things should be output, because you know the context and which content you trust or not. The exact same data can be dangerous in one context and harmless in another:

Assume you have a variable color that should contain color names (like red, green, rebeccapurple, …) and a template that should output the name of the color inside a box with a background of this color. Maybe like so:

  .box { background: <?= $this->color ?> }


<div class="box"><?= $this->color ?></div>

This is dangerous. The content of the variable has a different meaning when output in CSS or HTML! This gets particularly bad if the sanitization logic treating the input does not know about the different cases.

red; } { body: display:none;

A perfectly valid, safe value for color in the HTML context would effectively produce this style - certainly not what we want:

  .box { background: red; } body { display: none; }

Similar, stripping/encoding seemingly dangerous characters in CSS like ;, } or { would still allow an input like this which again would produce unwanted HTML:

<div class="box"><script>alert(1)</script></div>

This is a dilemma. The logic storing and processing data typically cannot know (or only assume) how the data will be used. Will this end up in an HTML document or inside an HTML tag? Or as a property in JSON-LD? Or as a value in a CSV file? …

With Twig we can be specific how a certain variable should be treated. Use the |escape or - short - |e filter for this:

  .box { background: {{ color|e('css') }} }


<div class="box">{{ color|e('html') }}</div>

✔️ Now, our “bad” input will be properly escaped for CSS or HTML and wouldn’t do any harm anymore:

.box { background: red\3B \20 \7D \20 \7B \20 body\3A \20 display\3A none\3B  }
<div class="box">&lt;script&gt;alert(1)&lt;/script&gt;</div>

Note: By default Twig encodes all variables. The chosen escaper strategy will depend on the template’s file extension: your .html.twig templates will automatically get the |e('html') treatment, so you could omit this part in the above example.

Try it out for yourself in this TwigFiddle.

Trusted raw data

If you intentionally do want to output a variable containing raw HTML, like <b>nice</b>, you need to add the |raw escaper filter to your variable {{ tiny_mce_content|raw }} which tells Twig to skip escaping this value. Otherwise &lt;b&gt;nice&lt;/b&gt; will be output, i.e. a text saying <b>nice</b> and not a bold word nice.

Keep in mind, that you only ever add |raw to trusted input! Using |raw on anything else may result in severe XSS vulnerabilities!

Our Twig implementation makes sure you can use Twig templates as you would with output encoding! So this strategy will be the same when we’re completely switching to output encoding one day.
In case you’re wondering how we achieve this: Under the hood we use our own contao_html and contao_html_attr escaper variants. These prevent double encoding and are used instead of the original ones for all @Contao* namespaced templates.

Extending Twig

In Twig you can easily use and write extensions. The Contao core for instance uses the KnpTimeBundle to format dates/time in a nice way (“5 minutes ago”). As this bundle provides a Twig extension with an |ago filter, you can directly use this functionality in your templates:

<p>Last edited: {{ modified_at|ago }}</p>

{# <p>Last edited: 5 minutes ago</p> #}

Using twig-extra bundles

In fact, there are already a lot of Twig extensions in the wild, including some official ones. These “twig-extra” bundles can simply be installed with composer and are directly ready to be used (no need to configure or register in the kernel).

composer require twig/intl-extra
{{ '1000000'|format_currency('EUR') }}

{# €1,000,000.00 #}

Make it your own

Twig has lots of extension points. The easiest things to add are filters and functions. Have a look at the offical docs where things are explained in detail. For fun, we are now going to implement a simple |rot13 filter in our application that will scramble strings by shifting every letter by 13 places in the alphabet (AbcdNopq).

If you are using the Symfony maker bundle you can use the command make:twig-extension to create a new extension. Otherwise, go ahead and create a class extending AbstractExtension in your src/Twig folder yourself. You can have as many extensions as you want but in an application you would typically use a single AppExtension until things get too crowded:

// src/Twig/AppExtension.php

namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class AppExtension extends AbstractExtension
    public function getFilters(): array
        return [
            new TwigFilter('rot13', [$this, 'rotateString']),

    public function rotateString(string $value): string
        return str_rot13($value);

That’s it - our filter is now ready to be used in any template:

{% set secret_cms = 'Pbagnb' %}

Turns out "{{ secret_cms }}" means "{{ secret_cms|rot13 }}".

{# Turns out "Pbagnb" means "Contao". #}