---
title: "Twig templates - a drupal cheat sheet"
description: "A Drupal Twig cheat sheet: every Twig tag, filter, function, test and operator, plus Drupal's own extras, Twig Tweak, debugging and the gotchas."
url: https://www.pivale.co/blog/twig-templates-drupal-cheat-sheet/
date: 2026-05-11
author: "Darren Fisher"
tags: ["Drupal", "Frontend", "Tutorial"]
publisher: Pivale (https://www.pivale.co)
---

# Twig templates - a drupal cheat sheet

Twig is awesome. It gives you a lot of the power of PHP but is far more readable and blends nicely with writing standard HTML. It is fast, secure, and flexible. Twig is used by many open-source projects including Symfony and by extension Drupal. If you're looking for a cheat sheet for Twig in Drupal, look no further! We cover variables, delimiters, tags, filters, functions, tests &amp; operators, and then everything Drupal adds on top: its own filters and functions, the Attribute object, template naming, debugging, single directory components and the Twig Tweak module.

Along the way I've added the tricks, quirks and "why on earth is it doing that?" moments I've collected from years of building Drupal themes. Bookmark it, search it with Ctrl/⌘ F, and come back whenever a template is fighting you. Enjoy!

## Which versions this covers

This cheat sheet is written for Drupal 10 and 11, which both use Twig 3. Twig has been adding features at a steady pace (Drupal 11.4 requires Twig 3.28 or later), so where something needs a recent version of Twig or Drupal, I've said so. To check which version of Twig your site has, run:

```shell
composer show twig/twig
```

Drupal 8 used Twig 1 and Drupal 9 used Twig 2. A few things changed in Twig 3 (the `if` inside a `for` loop and the `spaceless` tag are gone, for example), and old Stack Overflow answers are full of them, so I've flagged those too.

## Twig variables & delimiters

Twig prints variables and expressions using double curly braces like this: `{{ ... }}`.

Variables can have attributes or child elements that you can also access using a dot such as `{{ var.id }}` or `{{ item.child }}`.

Twig templates have two primary delimiters:

`{{ ... }}`: outputs a variable or the result of an expression.

`{% ... %}`: executes statements such as for, if else, set etc.

Here is a full example:

```twig
{{ page_title }}
<ul>
  {% for item in items %}
    <li>
      {% if item.primary %}
        <strong>{{ item.title }}</strong>
      {% else %}
        {{ item.title }}
      {% endif %}
    </li>
  {% endfor %}
</ul>
```

Twig templates also come with an additional delimiter which can be used for comments which looks like `{# ... #}`.

A common mix-up: the curly braces belong to the print statement, not the variable. Inside a tag you use the variable on its own, so it's `{% if item.primary %}`, never `{% if {{ item.primary }} %}`.

### Comments

`{# ... #}` comments can span several lines and never reach the browser, unlike HTML comments. From Twig 3.15 you can also add inline comments inside an expression with `#`. They run to the end of the line, so the closing braces have to go on the next line:

```twig
{# This comment never reaches the HTML. #}
{{
  'Hello'|upper # an inline comment (Twig 3.15+)
}}
```

### Whitespace control

Twig removes the first newline after a tag and leaves every other bit of whitespace alone. That's usually fine, but it bites when whitespace matters, such as inline elements or a comma-separated list. Add `-` to a delimiter to remove all whitespace (newlines included) on that side, or `~` to remove spaces and tabs but keep newlines:

```twig
<li>
  {{- item.title -}}
</li>
{# outputs <li>Title</li> #}

{%- if show_tags -%}
  {{ tags }}
{%- endif -%}
```

To remove the whitespace between two tags, use an empty comment with the trimming modifier on both sides: `{#--#}`.

<strong>Drupal quirk:</strong> with Twig debug switched on, Drupal wraps every template in HTML comments, which adds whitespace of its own. If spacing looks different with debug on and off, that's why.

### Accessing attributes and array keys

The dot works on arrays and objects alike. For `{{ user.name }}`, Twig tries, in order: an array key called `name`, a property called `name`, then the methods `name()`, `getName()`, `isName()` and `hasName()`. If none of those exist, you get null.

```twig
{{ node.title.value }}       {# dot notation #}
{{ content['field_tags'] }}  {# square brackets: needed for keys with hyphens or starting with a number #}
{{ content[field_name] }}    {# square brackets with a variable as the key #}
{{ node.('field_' ~ name) }} {# dynamic attribute (Twig 3.15+; the attribute() function before that) #}
{{ node?.label }}            {# null-safe: prints nothing if node is null (Twig 3.23+) #}
```

<strong>Drupal quirk:</strong> Drupal runs every template in a sandbox that limits which methods you can call on objects. See [the Twig sandbox in Drupal](#the-twig-sandbox-in-drupal) further down before you try `{{ node.toUrl() }}`.

### Literals

```twig
{% set single = 'Single quotes' %}
{% set double = "Double quotes allow #{interpolation}" %}
{% set number = 1_000_000 %}             {# underscores in numbers need Twig 3.17+ #}
{% set list = ['apple', 'pear'] %}       {# a sequence #}
{% set map = { colour: 'teal', 'data-id': 7, (key_var): 'dynamic key' } %}
{% set flags = [true, false, null] %}
```

String interpolation (`#{...}`) only works in double-quoted strings. Mapping keys don't need quotes unless they contain characters like hyphens, and you can use a variable as a key by wrapping it in brackets.

### Global variables

Twig gives every template `_self` (the current template's name), `_context` (every variable available to the template) and `_charset`. `_context` is a handy debugging tool, as you'll see in the debugging section.

Drupal adds its own defaults to every template:

- `attributes`, `title_attributes` and `content_attributes`: [Attribute objects](#the-attribute-object) for the wrapper, title and content.
- `title_prefix` and `title_suffix`: where modules put things like contextual links. Keep printing them!
- `directory`: the path to the active theme, such as `themes/custom/mytheme`. Handy for images: `<img src="/{{ directory }}/images/logo.svg" alt="">`.
- `logged_in`: true if the visitor is logged in.
- `is_admin`: true if the visitor has the "access administration pages" permission. It doesn't mean they have the administrator role!
- `user`: the current user's account.
- `db_is_active`: false on the maintenance page.

`is_front` is only set for the page template. If you need it in a node or block template, add it in a preprocess function.

### Undefined variables fail silently

Drupal runs Twig with `strict_variables` switched off. If you mistype a variable name, Twig prints nothing at all, with no error and no warning. When something isn't showing up, check the spelling before anything else. To check for a variable on purpose, use `is defined` or the `??` operator:

```twig
{% if subtitle is defined %}{{ subtitle }}{% endif %}
{{ subtitle ?? 'No subtitle' }}
```

## Twig tags

### {% apply %}

Applies twig filters on a block of code (rather than a single line of code which use the `|` syntax). You can chain filters too.

```twig
{% apply upper %}
  This text becomes uppercase.
{% endapply %}

{% apply lower|escape('html') %}
  <strong>SOME TEXT</strong>
{% endapply %}
```

### {% autoescape %}

Applies autoescape on a block of data. Can be combined with a space and an argument such as `'html'`, `'js'`, `'css'`, `'url'`, `'html_attr'` or `false`.

```twig
{% autoescape 'js' %}
  var title = '{{ title }}';
{% endautoescape %}
```

Drupal switches autoescaping on for every template and you should leave it that way. `{% autoescape false %}` turns off the protection against cross-site scripting (XSS) for everything inside it, so think hard before you use it.

### {% block %}

Declares or overrides a twig block. Twig blocks can be named such as `{% block named_block %}`. Twig blocks can only be overridden when using the `{% extends %}`, `{% embed %}` or `{% use %}` tags in another twig template.

```twig
{% block title %}
  <h1>{{ label }}</h1>
{% endblock %}

{# Shorthand for a block that prints one thing: #}
{% block subtitle subtitle|upper %}
```

Twig blocks have nothing to do with Drupal blocks, the things you place in regions. Same word, completely different meaning!

### {% cache %}

Caches a fragment of a template under a key, with an optional time to live in seconds: `{% cache "some cache key" ttl(5184000) %}`.

<strong>This one doesn't work in Drupal.</strong> The `cache` tag comes from the separate `twig/cache-extra` package, which Drupal doesn't include, so you'll get an "Unknown tag" error if you copy it from a Symfony example. Drupal has its own, much more capable caching through render arrays and cache metadata (tags, contexts and max-age). If a part of a template is expensive, cache it in PHP: move it into a render array with a `#cache` key or a lazy builder. See [Cache API](https://www.drupal.org/docs/drupal-apis/cache-api) on drupal.org.

### {% deprecated %}

Declares a deprecation notice for something in your template to help contributors migrate when changes are made. Must be combined with a deprecation notice in quotes. For example: `{% deprecated 'The "page.twig" template is deprecated. Please use the "node.twig" template instead.' %}`

From Twig 3.11 you can add the package and version: `{% deprecated 'Use card.html.twig instead.' package='mytheme' version='2.0' %}`. The notice is triggered when the template compiles, so it shows up in your logs and test runs, not on the page.

### {% do %}

Performs an operation but doesn't print it. For example `{% do 2 + 2 %}`. This might be useful if you want to perform an operation on an array in a loop based on a condition or something similar, without printing the result.

In Drupal this is the neat way to change an Attribute object without printing it:

```twig
{% do attributes.addClass('card', 'card--' ~ view_mode) %}
{% do attributes.setAttribute('data-id', node.id) %}

<article{{ attributes }}>
  ...
</article>
```

From Twig 3.23 you can also use `do` with the `=` operator to set variables, including destructuring: `{% do [first, last] = ['Darren', 'Fisher'] %}`.

### {% embed %}

Calls another twig template and allows you to <strong>pass variables using the with keyword</strong> and <strong>override any declared twig blocks in the template</strong> you are embedding. Here is a full example:

```twig
{% embed '@mytheme/components/card.html.twig' with {
  card_heading: heading,
  card_copy: copy,
  card_link: link,
} %}
  {% block card_img %}
    <img src="{{ image.src }}" alt="{{ image.alt }}"/>
  {% endblock %}
{% endembed %}
```

Like `include`, the embedded template sees all the current template's variables as well as the ones you pass. Add `only` after the `with` mapping to pass just those. In Drupal, `embed` is also how you fill the slots of a [single directory component](#single-directory-components-sdc).

### {% extends %}

Calls another twig template and allows you to <strong>override any declared twig blocks in the template</strong> you are extending, but you cannot pass variables using the with keyword and you cannot add any content outside of the blocks, making this suitable for vertical inheritance where you want to override some blocks from a parent template. Here is a full example:

```twig
{% extends '@mytheme/layout.html.twig' %}

{% block title %}
  {{ page_title }}
{% endblock %}

{% block content %}
  {{ content }}
{% endblock %}

{% block sidebar %}
  {{ parent() }}
  {{ sidebar_region }}
{% endblock %}
```

`{{ parent() }}` prints the parent template's version of the block, so you can add to it instead of replacing it. Since Twig 3.28, putting `extends` inside an `if` or another tag is deprecated. To choose a parent template conditionally, use an expression instead: `{% extends featured ? '@mytheme/layouts/featured.html.twig' : '@mytheme/layouts/default.html.twig' %}`.

In Drupal, extending a template from another theme or module is a great way to override one block of a template without copying the whole file. For example, a theme can extend one of core's Olivero templates with `{% extends '@olivero/content/node.html.twig' %}` and change a single block. The parent template has to declare blocks for this to work, and plenty of core templates have few or none.

<strong>Drupal trick:</strong> Drupal looks up plain template names such as `'node.html.twig'` in the theme registry. So in `node--article.html.twig`, `{% extends 'node.html.twig' %}` extends whichever `node.html.twig` your site is using (your theme's, or your base theme's), and your article template only has to override the blocks that differ.

### {% flush %}

Calls PHP's `flush()` function, which in theory sends everything rendered so far to the browser so visitors can start reading a long page while the rest is still rendering.

<strong>Don't bother with it in Drupal.</strong> Drupal renders each template into a string as part of a render array and only sends the response once the whole page is built, so `flush` has nothing useful to send. If part of a page is slow, Drupal's answer is [BigPipe](https://www.drupal.org/docs/8/core/modules/big-pipe/overview) (in core and switched on in the standard profile) together with lazy builders, which stream slow, personalised parts of the page in after the rest.

### {% for %}

Initiates a loop which iterates over items in an array. Once inside the loop you have access to each object's values (if applicable) and a special loop object that gives you some useful variables.

Here is an example:

```twig
{% for item in items %}
  <div>
    <h3>{{ item.title }}</h3>
    <p>{{ item.description }}</p>
    <a href="{{ item.link_url }}">{{ item.link_text }}</a>
  </div>
{% else %}
  <p>{{ 'Nothing to show yet.'|t }}</p>
{% endfor %}
```

The `else` part runs when there's nothing to loop over.

The special loop object variables are as follows:

- `loop.index` which returns the current index of the loop (starting from 1).
- `loop.index0` which returns the current iteration of the loop (starting from 0).
- `loop.revindex` which returns the number of iterations until the end of the loop (starting from 1).
- `loop.revindex0` which returns the number of iterations until the end of the loop (starting from 0).
- `loop.first` which returns true if the first iteration of the loop, otherwise returns false.
- `loop.last` which returns true if the last iteration of the loop, otherwise returns false.
- `loop.length` which returns the number of items in the overall sequence.
- `loop.parent` which returns the parent context.

`loop.length`, `loop.last`, `loop.revindex` and `loop.revindex0` aren't available when you loop over an iterator that can't be counted.

You can loop over keys and values, a range of numbers, or letters:

```twig
{% for key, value in mapping %}{{ key }}: {{ value }}{% endfor %}
{% for i in 1..5 %}{{ i }}{% endfor %}
{% for letter in 'a'..'e' %}{{ letter }}{% endfor %}
```

<strong>Changed in Twig 3:</strong> `{% for item in items if item.active %}` no longer works. Use the [filter](#filter) filter instead: `{% for item in items|filter(i => i.active) %}`. You also can't `break` or `continue` out of a Twig loop, so filter the items before the loop starts.

Loops have their own scope. A variable you create inside a loop disappears when the loop ends, but if you create it before the loop, you can change it inside:

```twig
{% set total = 0 %}
{% for item in items %}
  {% set total = total + item.price %}
{% endfor %}
{{ total }}
```

### {% from %}

Imports macro names into the current namespace. See the `{% macro %}` tag for more information.

```twig
{% from '@mytheme/macros/forms.html.twig' import input, textarea as text_area %}
{{ input('name') }}
```

### {% guard %}

New in Twig 3.15. Checks whether a function, filter or test exists when the template compiles, and lets you provide a fallback. This is gold for contributed themes and starter kits that need to support several versions of Drupal:

```twig
{% guard function html_classes %}
  <div class="{{ html_classes('card', { 'card--featured': featured }) }}">
{% else %}
  <div class="card{{ featured ? ' card--featured' }}">
{% endguard %}
```

### {% if %}

Runs a block of code only when a condition is true, with optional `elseif` and `else` branches. You can combine conditions with `and`, `or` and `not`, and use any [test](#tests) or [operator](#operators).

```twig
{% if node.isPublished() and not node.isPromoted() %}
  ...
{% elseif user.hasPermission('administer nodes') %}
  ...
{% else %}
  ...
{% endif %}
```

Twig follows PHP's rules for truthiness: an empty string, `0`, `'0'`, an empty array, null and false are all false.

<strong>Drupal quirk:</strong> `{% if content.field_image %}` is almost always true, even when the field is empty, because the render array still carries cache metadata. See [is this field empty?](#is-this-field-empty) for the fix.

You can also use `if` inline as a ternary: `{{ featured ? 'Featured' : 'Standard' }}`, and leave out the else part: `{{ featured ? 'Featured' }}`.

### {% import %}

Imports macros from a template into a variable, so you call them like methods. Macros in the same template are available through `_self` automatically.

```twig
{% import '@mytheme/macros/forms.html.twig' as forms %}
{{ forms.input('email', '', 'email') }}

{# Macros defined in this template: #}
{{ _self.input('name') }}
```

### {% include %}

Includes another template and prints its output. The included template sees all the current variables, and you can pass more with `with`. Add `only` to pass nothing but the variables you list, which I'd recommend for anything you reuse: it stops the included template depending on whatever happened to be lying around.

```twig
{% include '@mytheme/partials/teaser.html.twig' %}
{% include '@mytheme/partials/teaser.html.twig' with { title: label } %}
{% include '@mytheme/partials/teaser.html.twig' with { title: label } only %}
{% include '@mytheme/partials/promo.html.twig' ignore missing %}
{% include ['@mytheme/partials/teaser--' ~ bundle ~ '.html.twig', '@mytheme/partials/teaser.html.twig'] %}
```

`ignore missing` prints nothing if the file doesn't exist. A list of templates uses the first one that exists.

There's also an `include()` function that does the same thing and returns the output, so you can filter it or store it: `{% set teaser = include('@mytheme/partials/teaser.html.twig', { title: label }, with_context = false) %}`. From Twig 3.29 the `include_only()` function is a shortcut for the version without the current context.

In Drupal, use the `@theme_or_module_name/` namespace prefix to include a file from any theme or module. See [including templates from other themes and modules](#including-templates-from-other-themes-and-modules).

### {% macro %}

Defines a reusable chunk of template that takes arguments, like a function. Macros are handy for repeated markup that's too small to be worth its own template.

```twig
{% macro input(name, value = '', type = 'text') %}
  <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{{ _self.input('email', '', 'email') }}
```

Two gotchas. First, a macro can't see the template's variables, only its arguments, so pass in everything it needs. Second, recent versions of Twig are tightening up the rules: since Twig 3.29, calling a macro without parentheses, with the wrong case, or with an argument it doesn't declare triggers a deprecation notice, and these become errors in Twig 4. Always call macros with brackets.

In Drupal, a macro is the standard way to render nested menus, because a macro can call itself. Core's `menu.html.twig` does exactly this.

### {% sandbox %}

Runs an included template under the sandbox's security policy. It was deprecated in Twig 3.15 and is pointless in Drupal anyway, because Drupal already sandboxes every template. Skip it.

### {% set %}

Creates or changes a variable. You can set several at once, or capture a chunk of output with the block form:

```twig
{% set heading = label|upper %}
{% set first, last = 'Darren', 'Fisher' %}
{% set classes = ['card', featured ? 'card--featured'] %}

{% set byline %}
  {{ 'By'|t }} <span class="author">{{ author_name }}</span>
{% endset %}
{{ byline }}
```

The block form is safe to print: Twig treats the captured output as already escaped.

To add to an array, merge a new one in. Twig arrays can't be changed in place, so you reassign the variable:

```twig
{% set classes = classes|merge(['card--' ~ view_mode|clean_class]) %}
```

From Twig 3.23 you can also set variables with the `=` operator inside expressions, and split sequences and mappings into separate variables (destructuring):

```twig
{% do [first, , third] = ['a', 'b', 'c'] %}
{% do { title, url: link } = item %}   {# renames url to link (Twig 3.24+) #}
```

Variables you set inside an included template or a loop stay there. They don't leak back into the template that called them.

### {% trans %} (Drupal)

Drupal's own tag for translatable text, with placeholders and plurals. Variables inside are escaped and turned into `@` placeholders; add the `placeholder` filter to get a `%` placeholder, which Drupal wraps in `<em class="placeholder">`.

```twig
{% trans %}Submitted by {{ author_name }} on {{ date }}{% endtrans %}

{% trans %}
  Found one result for {{ query|placeholder }}.
{% plural count %}
  Found {{ count }} results for {{ query|placeholder }}.
{% endtrans %}

{% trans with { context: 'Long month name' } %}May{% endtrans %}
```

Keep the text inside `trans` simple: plain text and printed variables only. Attributes such as `{{ node.label }}` and filters are fine, but tags such as `if`, function calls and `~` joins aren't, and you'll get "The text to be translated with "trans" can only contain references to simple variables". Set a variable before the tag instead.

### {% types %}

Experimental, added in Twig 3.13. Declares the types of the variables a template expects. Twig doesn't check them at run time; it's documentation for people and for editors that understand it.

```twig
{% types {
  title: 'string',
  featured: 'bool',
  image?: 'string',
} %}
```

### {% use %}

Imports the blocks from another template so you can use them in this one, without extending it. It's called horizontal reuse, and it's useful when you want blocks from several templates at once. You can rename blocks as you import them.

```twig
{% extends '@mytheme/layout.html.twig' %}
{% use '@mytheme/blocks/cards.html.twig' with card as base_card %}

{% block card %}
  <div class="card--wrapper">{{ block('base_card') }}</div>
{% endblock %}
```

### {% verbatim %}

Outputs everything inside it as raw text without Twig parsing it. Use it when you need to print something that looks like Twig, such as a JavaScript template or a code sample:

```twig
{% verbatim %}
  <script type="text/x-template">{{ message }}</script>
{% endverbatim %}
```

### {% with %}

Creates an inner scope. Variables you set inside disappear at `endwith`. Add a mapping to add variables, and `only` to hide everything else:

```twig
{% with { title: 'Teaser' } only %}
  {{ title }}     {# Teaser #}
  {{ label }}     {# nothing: hidden by "only" #}
{% endwith %}
```

## Filters

Filters change a value. They follow a pipe (`|`) and can be chained: `{{ name|striptags|title }}`. Filters bind more tightly than any other operator, so wrap complex expressions in brackets: `{{ ('Hello ' ~ name)|upper }}`.

Several filters take an arrow function: a short function written `item => item.price`, with brackets around more than one argument: `(item, key) => key != 'skip'`.

### abs

Returns the absolute value of a number: `{{ -7|abs }}` gives 7... except it doesn't! The filter binds more tightly than the minus sign, so that's `-(7|abs)`, which is -7. Write `{{ (-7)|abs }}`.

### batch

Splits a sequence into groups of a given size, optionally padding the last group. Perfect for building rows:

```twig
{% for row in items|batch(3, '') %}
  <div class="row">
    {% for item in row %}<div class="col">{{ item }}</div>{% endfor %}
  </div>
{% endfor %}
```

### capitalize

Uppercases the first character and lowercases the rest: `{{ 'hELLO world'|capitalize }}` gives "Hello world".

### column

Returns the values of one key from every item in a sequence: `{{ products|column('price')|join(', ') }}`.

### convert_encoding

Converts a string from one character encoding to another: `{{ data|convert_encoding('UTF-8', 'iso-2022-jp') }}`. Needs PHP's iconv or mbstring extension.

### date

Formats a date or timestamp with PHP date format characters: `{{ 'now'|date('j F Y') }}`, `{{ timestamp|date('Y-m-d', 'Europe/London') }}`. The default format is `F j, Y H:i`.

<strong>In Drupal, prefer [format_date](#format_date).</strong> Twig's `date` doesn't translate month and day names, and doesn't know about Drupal's date formats.

### date_modify

Changes a date: `{{ 'now'|date_modify('+1 week')|date('j F Y') }}`.

### default

Returns a fallback when the value is undefined or empty: `{{ subtitle|default('No subtitle') }}`. "Empty" means an empty string, empty array, null or false; `0` isn't empty.

<strong>Drupal quirk:</strong> `{{ content.field_subtitle|default('...') }}` never shows the fallback, because a Drupal render array is never empty.

### escape (or e)

Escapes a value for a particular context: `html` (the default), `js`, `css`, `url` or `html_attr` (for attribute values without quotes). Drupal already escapes everything you print for HTML, so you only need this for other contexts:

```twig
<script>var searchTerm = '{{ query|e('js') }}';</script>
<a href="/search?q={{ query|e('url') }}">
```

Drupal replaces Twig's escape filter with its own version that understands render arrays and Drupal's safe markup objects.

### filter

Keeps the items for which an arrow function returns true. The function receives the value and, optionally, the key:

```twig
{% for product in products|filter(p => p.in_stock) %}...{% endfor %}
{% for key, child in content.field_tags|filter((v, k) => k|first != '#') %}...{% endfor %}
```

### find

New in Twig 3.11. Returns the first item for which an arrow function returns true, or null: `{% set lead = people|find(p => p.role == 'Director') %}`.

### first

Returns the first item of a sequence or mapping, or the first character of a string: `{{ [1, 2, 3]|first }}`.

### format

Fills placeholders in a string, like PHP's sprintf: `{{ '%s has %d items'|format(name, count) }}`.

Don't use it for text people read in Drupal: use [t](#t-and-trans) with placeholders so the text can be translated.

### invoke

New in Twig 3.19. Calls an arrow function stored in a variable: `{% set double = x => x * 2 %}{{ double|invoke(4) }}`.

### join

Joins the items of a sequence into a string, with an optional different glue before the last item:

```twig
{{ ['Drupal', 'Twig', 'Symfony']|join(', ', ' and ') }}
{# Drupal, Twig and Symfony #}
```

<strong>In Drupal, use [safe_join](#safe_join)</strong> when the items might be render arrays or markup.

### json_encode

Converts a value to JSON: `{{ settings|json_encode }}`. Add `|raw` only when printing straight into a `<script>` tag, and only with data you trust. For passing settings to JavaScript, Drupal's `drupalSettings` (set in PHP) is the better route.

### keys

Returns the keys of a mapping: `{% for key in _context|keys %}{{ key }} {% endfor %}`.

### last

Returns the last item of a sequence or mapping, or the last character of a string: `{{ breadcrumbs|last }}`.

### length

Returns the number of items in a sequence or mapping, or the number of characters in a string: `{% if items|length > 3 %}`.

<strong>Drupal quirk:</strong> on a render array, `length` counts every key, including the `#` properties, so `content.field_tags|length` isn't the number of tags. Count the field instead: `{{ node.field_tags|length }}`.

### lower

Lowercases a string: `{{ 'HELLO'|lower }}`.

### map

Applies an arrow function to every item: `{{ people|map(p => p.first ~ ' ' ~ p.last)|join(', ') }}`. Like `filter`, it receives the key too.

### merge

Merges two sequences, or two mappings. In mappings, later keys overwrite earlier ones:

```twig
{% set classes = ['card']|merge(['card--wide']) %}
{% set options = { size: 'small' }|merge({ colour: 'teal' }) %}
```

### nl2br

Converts newlines to `<br>` tags. It escapes the string first, so it's safe: `{{ address|nl2br }}`.

### number_format

Formats a number with grouped thousands: `{{ 1234.5|number_format(2, '.', ',') }}` gives 1,234.50.

### raw

Marks a value as safe so Twig doesn't escape it. <strong>Use it as little as possible.</strong> Anything you print with `|raw` that contains user input is a cross-site scripting (XSS) hole. In Drupal you rarely need it: rendered fields, `t()` output and other Drupal markup are already marked safe. If you find yourself reaching for `|raw`, fix the variable in a preprocess function instead (for example, with `Markup::create()` on markup you've built and checked).

### reduce

Reduces a sequence to a single value with an arrow function and a starting value: `{{ items|reduce((total, item) => total + item.price, 0) }}`.

### replace

Replaces parts of a string using a mapping: `{{ 'I like %this%'|replace({'%this%': fruit}) }}`.

### reverse

Reverses a sequence, mapping or string: `{{ items|reverse }}`. Pass `true` to keep the keys.

### round

Rounds a number to a given precision, using `common` (the default), `ceil` or `floor`: `{{ 42.55|round(1, 'floor') }}` gives 42.5.

### shuffle

New in Twig 3.11. Shuffles a sequence or string randomly. Watch out in Drupal: the result is cached with the rest of the template, so "random" stays the same until the cache clears. Randomise in JavaScript instead.

### slice

Extracts part of a sequence or string. There's a shorthand using square brackets:

```twig
{{ items|slice(0, 3) }}   {# the first three #}
{{ items[0:3] }}          {# the same #}
{{ 'Twig'[1:] }}          {# wig #}
```

### sort

Sorts a sequence, optionally with an arrow function using the `<=>` operator:

```twig
{% for product in products|sort((a, b) => a.price <=> b.price) %}...{% endfor %}
```

Keys are kept, which matters if you then loop with `key, value`.

### spaceless

Removed whitespace between HTML tags. <strong>Deprecated in Twig 3.12</strong> and removed in Twig 4. Use the [whitespace control](#whitespace-control) modifiers instead.

### split

Splits a string into a sequence: `{{ 'one,two,three'|split(',') }}`. An empty delimiter splits into chunks of a given size: `{{ 'abcd'|split('', 2) }}` gives `['ab', 'cd']`.

### striptags

Strips HTML tags and collapses whitespace. You can keep some tags: `{{ body|striptags('<p><a>') }}`.

### title

Capitalises the first letter of every word: `{{ 'my first car'|title }}` gives "My First Car".

### trim

Trims whitespace (or other characters) from the start and end of a string, or one side: `{{ '  hello  '|trim }}`, `{{ '--hello--'|trim('-', 'left') }}`.

### upper

Uppercases a string: `{{ 'hello'|upper }}`. For uppercase headings, CSS `text-transform: uppercase` is usually better, because screen readers read some uppercase words letter by letter.

### url_encode

Encodes a string for a URL, or turns a mapping into a query string: `{{ { q: query, page: 2 }|url_encode }}` gives `q=...&page=2`.

## Functions

Functions are called by name with brackets: `{{ range(1, 5) }}`. Since Twig 3.12, every function, filter and test accepts named arguments, which make long calls easier to read: `{{ range(low: 1, high: 10, step: 2) }}`.

### attribute

Gets an attribute of an object or array using a dynamic name: `{{ attribute(node, 'field_' ~ name) }}`. Deprecated in Twig 3.15: use the dot with brackets instead, `{{ node.('field_' ~ name) }}`.

### block

Prints a block from the current template (or another one) by name: `{{ block('title') }}`. Use it with the `defined` test to check for a block: `{% if block('footer') is defined %}`.

### constant

Returns the value of a PHP constant, or a class constant: `{{ constant('Drupal\\node\\NodeInterface::PUBLISHED') }}`. Note the double backslashes.

### cycle

Cycles through a list of values, which is handy for alternating classes: `<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">`.

### date

Turns a value into a date object so you can compare dates: `{% if date(node.created.value) < date('-30 days') %}Older than a month{% endif %}`.

### dump

Prints information about a variable (or every variable, with no arguments). In Drupal it only works when Twig debug mode is on, which keeps it from leaking onto a live site. See [dumping variables](#dumping-variables).

### enum and enum_cases

New in Twig 3.15 and 3.12. Work with PHP enums: `{{ enum('App\\Status').Active.value }}` and `{% for case in enum_cases('App\\Status') %}`. You won't need them often in Drupal, but they're there.

### include and include_only

The function versions of the [include](#-include-) tag. They return the output, so you can filter or store it. `include_only()` (Twig 3.29) doesn't pass the current variables.

### max and min

Return the largest or smallest value: `{{ max(1, 3, 2) }}`, `{{ min([4, 2, 8]) }}`.

### parent

Inside an overridden block, prints the parent template's version of that block: `{{ parent() }}`.

### random

Returns a random item, character or number: `{{ random(['teal', 'black', 'white']) }}`, `{{ random(10) }}`. As with [shuffle](#shuffle), Drupal's render cache will freeze the result.

### range

Returns a sequence of numbers or letters, with an optional step: `{% for year in range(2007, 'now'|date('Y')) %}`. The `..` operator is shorthand for it.

### source

Returns the raw contents of a file without rendering it as Twig. This is my favourite trick for inline SVG icons, because Drupal lets Twig load `.svg` files:

```twig
<button class="menu-toggle">
  {{ source('@mytheme/images/icons/menu.svg') }}
  <span class="visually-hidden">{{ 'Menu'|t }}</span>
</button>
```

The SVG goes straight into the HTML, so you can style it with CSS (`fill: currentColor`) and it doesn't need another request. Pass `ignore_missing = true` to print nothing if the file isn't there. Only use it for your own files: `source()` prints the file exactly as it is, without escaping.

### template_from_string

Renders a string as a Twig template. <strong>Not available in Drupal</strong>: it needs Twig's StringLoader extension, which Drupal doesn't load. That's a good thing; rendering Twig from a string is risky.

## Tests

Tests check a value and return true or false. Use them with `is` and `is not`: `{% if count is odd %}`.

### constant

Checks whether a value equals a constant: `{% if status is constant('Drupal\\node\\NodeInterface::PUBLISHED') %}`.

### defined

Checks whether a variable or attribute exists: `{% if subtitle is defined %}`, `{% if node.field_subtitle is defined %}`. Remember that undefined variables print nothing in Drupal, so this is how you tell "missing" from "empty".

### divisible by

Checks whether a number divides exactly: `{% if loop.index is divisible by(3) %}</div><div class="row">{% endif %}`.

### empty

Checks for an empty string, empty array, null or false: `{% if items is empty %}`. `0` and `'0'` aren't empty.

<strong>Drupal quirk:</strong> `content.field_image is empty` is false even when the field has no image. See [is this field empty?](#is-this-field-empty)

### even and odd

Check whether a number is even or odd: `{% if loop.index is even %}`.

### iterable

Checks whether a value can be looped over: `{% if items is iterable %}`.

### mapping and sequence

New in Twig 3.11. Check whether a value is a mapping (an array with keys, such as `{ a: 1 }`) or a sequence (a list, such as `[1, 2]`).

### null (or none)

Checks for null: `{% if subtitle is null %}`.

### same as

Checks whether two values are identical, the same as PHP's `===`: `{% if count is same as(false) %}`. From Twig 3.23 you can write `count === false` instead.

## Operators

### Maths

`+`, `-`, `*`, `/`, `//` (divide and round down), `%` (remainder) and `**` (power). For example, `{{ 20 // 7 }}` gives 2 and `{{ 2 ** 3 }}` gives 8.

### Logic

`and`, `or`, `not`, and `xor` (Twig 3.15). There are also bitwise operators: `b-and`, `b-or` and `b-xor`. Twig's operators are case sensitive, so it's `and`, not `AND`.

### Comparison

`==`, `!=`, `<`, `>`, `<=` and `>=` compare loosely, like PHP. From Twig 3.23, `===` and `!==` compare strictly (the same as the `same as` test). The spaceship operator `<=>` returns -1, 0 or 1, which is what [sort](#sort) needs.

### Containment

`in` and `not in` check whether a value is in a sequence, mapping or string:

```twig
{% if 'editor' in user.getRoles() %}...{% endif %}
{% if 'Drupal' in title %}...{% endif %}
```

`in` compares loosely, so `0 in ['a']` can surprise you. When types matter, compare strictly.

### Strings

`~` joins strings: `{{ 'node--' ~ node.bundle }}`. `starts with`, `ends with` and `matches` (a regular expression) check them:

```twig
{% if url starts with 'https://' %}...{% endif %}
{% if filename ends with '.pdf' %}...{% endif %}
{% if postcode matches '/^[A-Z]{1,2}\\d/' %}...{% endif %}
```

### Iterables

`has some` and `has every` check the items of a sequence with an arrow function:

```twig
{% if products has some p => p.on_sale %}<span>Sale on!</span>{% endif %}
{% if products has every p => p.in_stock %}...{% endif %}
```

For an empty sequence, `has every` is true and `has some` is false.

### Tests

`is` and `is not` apply a [test](#tests): `{% if count is not empty %}`.

### Everything else

- `..` makes a range: `1..5`.
- `|` applies a filter.
- `.` and `[]` get an attribute or key; `?.` does the same but returns null instead of failing when the left side is null (Twig 3.23).
- `?:` is the ternary operator, and `a ?: b` (the "Elvis" operator) returns `a` if it's truthy, otherwise `b`.
- `??` returns the left side if it's defined and not null, otherwise the right: `{{ subtitle ?? label }}`.
- `...` spreads a sequence or mapping into another, or into a function's arguments: `{% set all = [...defaults, ...extras] %}` (Twig 3.15).
- `=>` creates an arrow function.
- `=` assigns a value inside an expression (Twig 3.23).

### Precedence gotchas

When in doubt, add brackets. Twig 3.15 started warning about some combinations that will change in Twig 4: mixing `??` with other operators, `~` with `+` or `-`, and `not` with `*`, `/`, `//` or `%`. Brackets make the intent clear and keep you ready for Twig 4:

```twig
{{ (subtitle ?? 'Untitled') ~ ' | ' ~ site_name }}
{{ 'Total: ' ~ (price + vat) }}
```

## Drupal's own Twig filters

Drupal adds these filters to every template.

### t and trans

Translates interface text with Drupal's `t()` function. `trans` is an alias. Use placeholders for anything that changes: `@` escapes the value, `%` escapes it and wraps it in `<em class="placeholder">`, and `:` is for URLs in attributes:

```twig
{{ 'Read more'|t }}
{{ 'Submitted by @name on @date'|t({ '@name': author_name, '@date': date }) }}
{{ 'Read the <a href=":url">accessibility statement</a>.'|t({ ':url': path('entity.node.canonical', { node: 12 }) }) }}
{{ 'May'|t({}, { context: 'Long month name' }) }}
```

Only translate literal strings. `{{ some_variable|t }}` can't be picked up for translation and is unsafe if the variable contains user input. Translate content (fields) with Drupal's content translation instead.

### placeholder

Escapes a value and wraps it in `<em class="placeholder">`. It's mainly for `{% trans %}` tags, where it turns a variable into a `%` placeholder: `{% trans %}Searched for {{ query|placeholder }}{% endtrans %}`.

### escape, e and drupal_escape

Drupal replaces Twig's escape filter with one that understands render arrays and Drupal's safe markup objects. You'll rarely call it yourself: autoescaping does it for every `{{ }}`.

### safe_join

Joins items with a separator, escaping each one unless it's already safe markup. It renders render arrays too, which Twig's `join` can't:

```twig
{{ [author_name, date]|safe_join(' | ') }}
{{ content.field_tags|children|safe_join(', ') }}   {# children comes from Twig Tweak #}
```

The separator isn't escaped, so never use user input as the separator.

### without

Returns a copy of a render array without the keys you list, leaving the original untouched. The classic use is printing a node's content without the fields you've printed elsewhere:

```twig
<div class="node__media">{{ content.field_image }}</div>
<div class="node__body">{{ content|without('field_image', 'links') }}</div>
```

It works on Attribute objects too: `<div{{ attributes|without('id') }}>`.

### clean_class

Turns a string into a valid CSS class name: lowercase, with spaces and underscores as hyphens. `{{ 'Featured Article'|clean_class }}` gives `featured-article`.

### clean_id

Turns a string into a valid HTML ID: `{{ 'Contact Form'|clean_id }}` gives `contact-form`. It doesn't make the ID unique on the page; for that, use `clean_unique_id`.

### clean_unique_id

Added in Drupal 10.1. Like `clean_id`, but adds a suffix (`--2`, `--3`...) when the same ID has already been used on the page. Perfect for templates that appear more than once, such as an accordion with `aria-controls`:

```twig
{% set panel_id = 'faq-panel'|clean_unique_id %}
<button aria-controls="{{ panel_id }}" aria-expanded="false">{{ question }}</button>
<div id="{{ panel_id }}" hidden>{{ answer }}</div>
```

### add_class

Added in Drupal 10.1. Adds classes to a render array's wrapper, so you can style a field from the node template without overriding the field template:

```twig
{{ content.field_image|add_class('card__image') }}
{{ content.body|add_class('prose', 'card__body') }}
```

It works on render arrays, not on Attribute objects (use `attributes.addClass()` for those) or plain strings.

### set_attribute

Added in Drupal 10.1. Sets any HTML attribute on a render array's wrapper:

```twig
{{ content.field_gallery|set_attribute('data-gallery', 'main') }}
{{ content.body|set_attribute('id', 'main-content') }}
```

### render

Renders a render array into markup. Use it when you need the rendered output itself, for example to test whether it's empty, or to strip its tags:

```twig
{% set summary = content.body|render|striptags|trim %}
```

Rendering is the expensive part, so don't render the same thing twice: store it in a variable, as above, and print the variable.

### format_date

Formats a Unix timestamp using Drupal's date formats, in the visitor's timezone and language. Use a format's machine name, or `custom` with PHP date characters:

```twig
{{ node.created.value|format_date }}
{{ node.created.value|format_date('short') }}
{{ node.changed.value|format_date('custom', 'j F Y') }}
```

For date fields, see [dates](#dates).

### add_suggestion

Adds a template suggestion to a render array from inside a template, so it uses a different template. Hyphens in the suggestion are turned into underscores for you:

```twig
{{ content.field_tags|add_suggestion('inline-list') }}
{# Now looks for field--inline-list.html.twig before field.html.twig #}
```

The render array needs a `#theme` key, which fields, entities and most elements have.

## Drupal's own Twig functions

### active_theme and active_theme_path

Return the machine name of the active theme and its path: `{{ active_theme() }}` gives `mytheme`, and `{{ active_theme_path() }}` gives `themes/custom/mytheme`.

### attach_library

Attaches a CSS/JS library, defined in your theme's `*.libraries.yml`, to the page, only when the template is used: `{{ attach_library('mytheme/carousel') }}`. It prints nothing. This is the best way to load a component's CSS and JavaScript only on pages that have the component. Calling it more than once is fine: Drupal only adds a library once.

### create_attribute

Creates a new [Attribute object](#the-attribute-object), for elements inside your template that need their own attributes:

```twig
{% set link_attributes = create_attribute({ class: ['card__link'] }) %}
<a{{ link_attributes.setAttribute('href', url) }}>{{ label }}</a>
```

### file_url

Turns a file URI such as `public://images/photo.jpg` into a relative URL: `{{ file_url(node.field_image.entity.uri.value) }}`.

### link

Creates a link render array from text and a URL object or URI string, with optional attributes:

```twig
{{ link(item.title, item.url) }}
{{ link('Contact us'|t, 'internal:/contact', { class: ['button'] }) }}
{{ link('Home'|t, 'route:<front>') }}
```

A URI string needs a scheme: `internal:/path`, `entity:node/1`, `route:<front>` or a full `https://` address. A plain `/contact` won't work. Unlike a hand-written `<a>`, it handles menu links set to `<nolink>` and `<button>`.

### path and url

Generate a relative (`path`) or absolute (`url`) URL from a route name and parameters, with options such as a query string or fragment:

```twig
{{ path('<front>') }}
{{ path('entity.node.canonical', { node: 12 }) }}
{{ path('entity.taxonomy_term.canonical', { taxonomy_term: term.id }) }}
{{ path('view.blog.page_1', {}, { query: { page: 2 }, fragment: 'results' }) }}
{{ url('<current>') }}
```

Not sure of a route's name? `drush route` lists them all, or run `drush route --path=/node/1` for a single path.

### render_var

Renders a render array, string or object. Drupal wraps every `{{ }}` in it behind the scenes, so you'll rarely call it. It's occasionally handy for a quick inline render array: `{{ render_var({ '#markup': '<p>Hello</p>' }) }}`.

### icon

Added in Drupal 11.1 with the Icon API. Renders an icon from an icon pack declared in a module or theme's `*.icons.yml` file, with optional settings: `{{ icon('mytheme_icons', 'arrow-right', { size: 24 }) }}`. If the pack or icon doesn't exist, it prints nothing instead of failing.

### help_route_link and help_topic_link

Added by the Help module for help topic templates: they link to a route or another help topic only if the current user can access it. You won't need them in a theme.

### html_classes, html_cva and html_attr

Added in Drupal 11.4, which ships Twig's HTML extension. `html_classes` builds a class list from strings and conditions:

```twig
<div class="{{ html_classes('card', { 'card--featured': featured, 'card--dark': dark }) }}">
```

`html_cva` implements the Class Variance Authority pattern, popular with Tailwind and component libraries, for components with variants:

```twig
{% set button = html_cva(
  base: 'button',
  variants: {
    variant: { primary: 'button--primary', secondary: 'button--secondary' },
    size: { sm: 'button--sm', lg: 'button--lg' },
  },
  default_variant: { variant: 'primary' }
) %}
<button class="{{ button.apply({ variant, size }, extra_classes) }}">{{ label }}</button>
```

`html_attr` renders a set of attributes from one or more mappings, merging and escaping them: `<button {{ html_attr({ class: ['button'], disabled: is_disabled, 'aria-pressed': pressed }) }}>`. It leaves out null values, prints true as an empty attribute and turns booleans into `"true"` or `"false"` for `aria-*` attributes. Use it for new elements; where Drupal already gives you an `attributes` object, keep using that. The same extension adds the `data_uri`, `html_attr_merge` and `html_attr_type` filters.

## The Attribute object

Most Drupal templates get an `attributes` variable, and many get `title_attributes` and `content_attributes` too. They're Attribute objects: a collection of HTML attributes that modules, themes and preprocess functions have been adding to. Print them straight into the tag, with no space before them; the object adds its own leading space:

```twig
<article{{ attributes.addClass('card') }}>
  <h2{{ title_attributes.addClass('card__title') }}>{{ label }}</h2>
  <div{{ content_attributes }}>{{ content }}</div>
</article>
```

The methods you can call:

- `addClass('a', 'b')` or `addClass(['a', 'b'])`: adds classes (duplicates are ignored, and empty values in an array are skipped).
- `removeClass('a')`: removes classes.
- `hasClass('a')`: checks for a class.
- `setAttribute('name', 'value')`: sets an attribute.
- `removeAttribute('name')`: removes attributes.
- `hasAttribute('name')`: checks for an attribute.
- `getClass()`: returns the class attribute.
- `toArray()`: returns everything as an array.
- `merge(other)`: merges another Attribute object in.

The methods return the object, so you can chain them: `attributes.addClass('card').setAttribute('id', 'main')`. You can also read a single attribute with `{{ attributes.id }}` or print the classes with `{{ attributes.class }}`.

Core templates build their class lists like this, and it's a pattern worth copying. Empty values (from a ternary with no else) are skipped:

```twig
{% set classes = [
  'node',
  'node--type-' ~ node.bundle|clean_class,
  node.isPromoted() ? 'node--promoted',
  view_mode ? 'node--view-mode-' ~ view_mode|clean_class,
] %}
<article{{ attributes.addClass(classes) }}>
```

<strong>The big gotcha:</strong> these methods change the object itself, not a copy. If you call `attributes.addClass('a')` on one element and then print `attributes` on another, the second element gets class "a" too. And if an included template changes an Attribute object you passed in, the change shows up back in the parent. When you need a different set for a second element, make a copy with `attributes|without()` (which clones it) or start fresh with `create_attribute()`.

## Drupal specific twig templates

### Where templates come from

Every piece of HTML Drupal outputs comes from a template, provided by core, a module or a theme. To change one, copy it into your theme's `templates` folder (any subfolder works), change it, and clear the cache. Drupal picks the most specific template it can find, in this order: your theme, its base themes, then the module that defined it. The fastest way to find out which template made a piece of HTML is [Twig debug mode](#turn-on-twig-debug-mode).

Start from the template in your base theme, or from core's Stable 9 theme if you don't have one (`core/themes/stable9/templates`), rather than the module's own version, so your markup doesn't change when the module is updated.

### Template naming conventions

Template file names are built from the theme hook plus more specific parts, separated by double hyphens. The most specific match wins. Underscores in machine names become hyphens in file names: a content type called `case_study` gives `node--case-study.html.twig`.

- <strong>HTML:</strong> `html--node--12.html.twig`, `html--node.html.twig`, `html.html.twig`
- <strong>Page:</strong> `page--node--edit.html.twig`, `page--node--12.html.twig`, `page--node.html.twig`, `page--front.html.twig`, `page.html.twig`
- <strong>Region:</strong> `region--sidebar-first.html.twig`, `region.html.twig`
- <strong>Block:</strong> `block--[module]--[delta].html.twig`, `block--[module].html.twig`, `block.html.twig`. For example, `block--system-branding-block.html.twig`, or for a Views block, `block--views-block--front-news-block-1.html.twig`
- <strong>Node:</strong> `node--12--teaser.html.twig`, `node--12.html.twig`, `node--article--teaser.html.twig`, `node--article.html.twig`, `node--teaser.html.twig`, `node.html.twig`
- <strong>Taxonomy term:</strong> `taxonomy-term--12.html.twig`, `taxonomy-term--tags.html.twig`, `taxonomy-term.html.twig`
- <strong>Field:</strong> `field--node--field-image--article.html.twig`, `field--node--field-image.html.twig`, `field--node--article.html.twig`, `field--field-image.html.twig`, `field--image.html.twig` (the field type), `field.html.twig`
- <strong>Comment:</strong> `comment--field-comments--article.html.twig`, `comment.html.twig`
- <strong>Views:</strong> `views-view--[view]--[display].html.twig`, `views-view--[view]--[display-type].html.twig`, `views-view--[display-type].html.twig`, `views-view--[view].html.twig`, `views-view.html.twig`. Row and field templates follow the same pattern: `views-view-unformatted--[view].html.twig`, `views-view-fields--[view].html.twig`, `views-view-field--[view]--[field].html.twig`
- <strong>Maintenance page:</strong> `maintenance-page.html.twig`

A page template for one content type, such as `page--article.html.twig`, doesn't exist out of the box. You add the suggestion yourself (see the next section).

### Adding your own template suggestions

To make Drupal look for a template it doesn't suggest by default, add a suggestion in your theme's `.theme` file:

```php
/**
 * Implements hook_theme_suggestions_HOOK_alter() for page templates.
 */
function mytheme_theme_suggestions_page_alter(array &$suggestions, array $variables) {
  if ($node = \Drupal::routeMatch()->getParameter('node')) {
    $suggestions[] = 'page__' . $node->bundle();
  }
}
```

Suggestions use double underscores and no hyphens (`page__case_study`); Drupal turns them into `page--case-study.html.twig`. If you get that wrong, Twig debug lists your suggestion under "INVALID FILE NAME SUGGESTIONS", which is a great clue. From inside a template, the [add_suggestion](#add_suggestion) filter does the same job for a single render array.

### Including templates from other themes and modules

Drupal registers a Twig namespace for every enabled theme and module. `@mytheme/` points at the theme's `templates` folder first, then the theme's root folder, so both of these work:

```twig
{% include '@mytheme/partials/card.html.twig' %}   {# themes/custom/mytheme/templates/partials/card.html.twig #}
{{ source('@mytheme/images/logo.svg') }}           {# themes/custom/mytheme/images/logo.svg #}
```

Twig in Drupal only loads files with certain extensions: `css`, `html`, `js`, `svg` and `twig`. That's why `source()` can inline an SVG but not a PNG. You can change the list with `twig.config: allowed_file_extensions` in your `services.yml`.

Including a file directly by path skips Drupal's theme system: no preprocess functions, no template suggestions and no Twig debug comments. For anything other than small partials, render a proper theme hook or a single directory component instead.

## Working with entities and fields

### content vs node

In an entity template such as `node.html.twig`, you get two very different things:

- `content`: render arrays for each field, built with the formatters and settings from "Manage display". Printing `{{ content.field_image }}` gives you the image formatted, with the right image style, field template, access checks and cache metadata.
- `node` (or `term`, `user`, `media`, `paragraph`...): the entity object itself, with its raw values. `{{ node.field_image.alt }}` gives you a single raw value, with no formatter, no template and no cache metadata.

<strong>Use `content` wherever you can.</strong> It keeps site builders in control through "Manage display", it's translated and access-checked, and its cache metadata keeps your pages up to date. Reach for raw values when you need something no formatter gives you, such as a value for a class name or a data attribute.

### Printing raw field values

```twig
{{ node.label }}                              {# the title #}
{{ node.id }}                                 {# the node ID #}
{{ node.bundle }}                             {# the content type's machine name #}
{{ node.field_subtitle.value }}               {# a text or number field #}
{{ node.body.value }}                         {# body text, unfiltered! #}
{{ node.body.summary }}                       {# the body's summary #}
{{ node.field_link.uri }}                     {# a link field's URI #}
{{ node.field_link.title }}                   {# a link field's text #}
{{ node.field_image.alt }}                    {# an image's alt text #}
{{ node.field_image.entity.uri.value }}       {# an image's file URI #}
{{ node.field_tags.entity.label }}            {# the first referenced term's name #}
{{ node.field_tags.target_id }}               {# the first referenced term's ID #}
{{ node.field_featured.value ? 'Featured' }}  {# a boolean field #}
{{ node.created.value }}                      {# a timestamp #}
```

`node.field_x.value`, `node.field_x[0].value` and `node.field_x.0.value` all give the first item's value.

<strong>Be careful with text fields:</strong> `node.body.value` is the raw text as stored, before Drupal's text formats have filtered it. Twig will escape it, so you'll see HTML tags printed as text; and if you add `|raw` to "fix" that, you've bypassed the filtering that keeps editors' HTML safe. Print `content.body` instead.

### Is this field empty?

This is the question I get asked most. `{% if content.field_image %}` is true even when there's no image, because Drupal still returns a render array carrying cache metadata. There are two good ways to check:

```twig
{# 1. Check the raw field. Fast, but ignores field access and display settings. #}
{% if not node.field_image.isEmpty() %}
  <div class="card__media">{{ content.field_image }}</div>
{% endif %}

{# 2. Render once, then check the output. Respects everything, costs a render. #}
{% set media = content.field_image|render %}
{% if media|trim is not empty %}
  <div class="card__media">{{ media }}</div>
{% endif %}
```

Don't add `|striptags` to the second version for image or media fields: it strips the `<img>` and every check comes back empty. The same trick works for regions in `page.html.twig`: a region that has blocks which render nothing still counts as "not empty", so render it first and check the result.

### Looping over field items

Inside a field template (`field--*.html.twig`), loop over `items` and print each `item.content`; `item.attributes` holds the item's attributes:

```twig
<ul{{ attributes.addClass('tag-list') }}>
  {% for item in items %}
    <li{{ item.attributes }}>{{ item.content }}</li>
  {% endfor %}
</ul>
```

Inside an entity template, `content.field_tags` mixes numbered items with `#` properties, so filter out the properties before you loop. Twig Tweak's [children](#children) filter does the same in fewer characters:

```twig
{% for key, tag in content.field_tags|filter((v, k) => k|first != '#') %}
  <li>{{ tag }}</li>
{% endfor %}
```

Or loop over the raw values: `{% for tag in node.field_tags %}{{ tag.entity.label }}{% endfor %}`.

### Entity references, access and translation

`node.field_author.entity` loads the referenced entity, so you can chain into its fields: `{{ node.field_author.entity.field_job_title.value }}`. Three things to know before you do:

- <strong>No access checks.</strong> You'll print a referenced entity even if it's unpublished or the visitor isn't allowed to see it.
- <strong>No translation.</strong> On a multilingual site, `.entity` gives you the referenced entity in its original language, not the language of the page. Twig Tweak's [translation](#translation) filter fixes that.
- <strong>No cache metadata.</strong> If the referenced entity changes, your cached page doesn't know. More on that under [cache metadata](#cache-metadata-and-why-your-change-isnt-showing).

If you need a referenced entity's fields, set the field's formatter to "Rendered entity" with a view mode instead, and theme that view mode's template. It handles all three.

### Links and URLs

Entity templates give you `url`, the entity's URL, so a teaser link is just `<a href="{{ url }}">{{ label }}</a>`. For other links, use [path and url](#path-and-url), or [link](#link) for a ready-made link.

Don't try `{{ node.toUrl() }}`. Drupal's sandbox blocks it (see the next section) and you'll get an error. The `url` variable, `path('entity.node.canonical', { node: node.id })` or Twig Tweak's [entity_url](#entity_url) all work.

### Images

The best way to print an image is through its formatter: `{{ content.field_image }}`, with the image style or responsive image style set in "Manage display". That way you get `srcset`, lazy loading, width and height attributes and cache metadata for free.

When you need just the URL (for a background image, say), Drupal core has `file_url()` but no way to apply an image style in Twig. Either add the URL in a preprocess function, or use Twig Tweak's [image_style](#image_style) filter:

```twig
{{ file_url(node.field_image.entity.uri.value) }}
{{ node.field_image.entity.uri.value|image_style('large') }}   {# needs Twig Tweak #}
```

For a media field, it's one level deeper: `node.field_media.entity.field_media_image.entity.uri.value`. Yes, really.

### Dates

For timestamps (the `created` and `changed` fields), use [format_date](#format_date). Date fields are trickier: the stored value is a string in UTC, such as `2026-05-11T09:00:00`, and Twig's `date` filter would read it in the wrong timezone. Convert it to a timestamp first, and let `format_date` handle the visitor's timezone and language:

```twig
{{ node.field_event_date.date.getTimestamp()|format_date('custom', 'j F Y, g:ia') }}
{{ content.field_event_date }}   {# or just use the formatter #}
```

### Translating text

Wrap every piece of interface text you write in a template in `t` or `trans`, even on a single-language site. It costs nothing, and it means the text can be changed through the interface translation pages without a deploy:

```twig
<button class="search-toggle">{{ 'Search'|t }}</button>
{% trans %}Posted {{ date }}{% endtrans %}
```

### Cache metadata, and why your change isn't showing

Drupal caches rendered output and uses cache metadata (tags, contexts and max-age) to know when a cached copy is out of date. Render arrays carry that metadata, and printing them passes it up the page. Raw values don't carry any. So when you print `node.field_author.entity.label`, Drupal doesn't know your output depends on that author, and when someone edits the author, the old name stays in the cache.

Two fixes:

```twig
{# 1. Print the render arrays you used, or at least render them, so their metadata comes along. #}
{% set catch_cache = content|render %}

{# 2. With Twig Tweak, attach just the metadata of what you used. #}
{{ content.field_author|cache_metadata }}
```

The first is common in contributed themes, but it renders all of `content`, which isn't free. Better still, prepare what you need in a preprocess function and add the right cache tags in PHP.

The same goes for `logged_in`, `is_admin` and `user` from the default variables: they don't add any cache contexts of their own. Drupal varies its caches by the visitor's permissions by default, which covers most cases, but if your template shows something different per user, add a `user` cache context in a preprocess function.

### The Twig sandbox in Drupal

Drupal runs every template in Twig's sandbox, with a policy that limits which methods you can call on objects. This stops a template from doing something destructive like `{{ node.delete() }}`. You can call:

- Any method on Attribute objects.
- `id()`, `label()`, `bundle()`, `get()`, `__toString()` and `toString()` on any object.
- Any method whose name starts with `get`, `has` or `is`, such as `node.getTitle()`, `node.hasField('field_tags')` and `node.isPublished()`.

Anything else, such as `node.toUrl()`, `node.access('update')` or `date.format('Y')`, fails with an error like this:

```text
Twig\Sandbox\SecurityError: Calling "toUrl" method on a "Drupal\node\Entity\Node" object is not allowed.
```

You can widen the policy in `settings.php` with `$settings['twig_sandbox_allowed_methods']`, `twig_sandbox_allowed_prefixes` and `twig_sandbox_allowed_classes`, but please don't: work out the value in a preprocess function and pass it to the template instead.

## Single directory components (SDC)

Single directory components keep a component's Twig, YAML schema, CSS and JavaScript together in one folder, such as `themes/custom/mytheme/components/card/`. They became part of Drupal core in 10.1 and are stable from Drupal 10.3. Drupal attaches the component's CSS and JavaScript for you whenever it's used.

You call a component by its ID, `[theme or module]:[component]`, with `include` for props or `embed` to fill its slots (slots are Twig blocks in the component's template):

```twig
{# Props only #}
{{ include('mytheme:card', {
  heading: label,
  url: url,
  variant: 'featured',
}, with_context = false) }}

{# Props and slots #}
{% embed 'mytheme:card' with { heading: label, url: url } only %}
  {% block media %}
    {{ content.field_image }}
  {% endblock %}
  {% block body %}
    {{ content.body }}
  {% endblock %}
{% endembed %}
```

Pass components only what they need (`only`, or `with_context = false`), so they behave the same wherever you use them. Drupal checks props against the component's schema, so a typo in a prop name or the wrong type shows up as an error while you develop rather than a blank space on the page. You'll also see `add_component_context()` and `validate_component_props()` in compiled templates; Drupal uses them internally, and you don't need to call them.

For how SDC compares with Storybook, see Pri's [Drupal SDC vs Storybook](https://www.pivale.co/blog/drupal-sdc-vs-storybook/) showdown.

## Debugging Twig in Drupal

### Turn on Twig debug mode

From Drupal 10.1, go to <strong>Configuration › Development › Development settings</strong> (`/admin/config/development/settings`) and tick "Twig development mode", then "Twig debug mode" and "Disable Twig cache". "Do not cache markup" is useful too, while you're working on templates.

On older versions, or to keep it in code, add this to `sites/development.services.yml` and include that file from `settings.local.php`:

```yaml
parameters:
  twig.config:
    debug: true
    auto_reload: true
    cache: false
```

Never switch debug mode on for a live site. The extra comments can break some output, such as views that produce JSON or plain text, and your tests may fail with it on.

### Reading the debug comments

With debug on, view the page source (not the browser's inspector, which can hide comments) and every template is wrapped in comments like these:

```html
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'node' -->
<!-- FILE NAME SUGGESTIONS:
   ▪️ node--12--full.html.twig
   ▪️ node--12.html.twig
   ▪️ node--article--full.html.twig
   ✅ node--article.html.twig
   ▪️ node--full.html.twig
   ▪️ node.html.twig
-->
<!-- 💡 BEGIN CUSTOM TEMPLATE OUTPUT from 'themes/custom/mytheme/templates/content/node--article.html.twig' -->
...
<!-- END CUSTOM TEMPLATE OUTPUT from 'themes/custom/mytheme/templates/content/node--article.html.twig' -->
```

The tick shows the template in use, the list shows every file name you could create to override it (most specific first), and the 💡 tells you it came from your theme rather than core or a module.

### Dumping variables

`{{ dump() }}` dumps every variable in the template and `{{ dump(label) }}` dumps one. When Symfony's VarDumper component is installed, Drupal uses it for collapsible, readable output. Both only work in debug mode.

<strong>Don't dump everything in an entity template.</strong> `dump()` on its own, or `dump(node)`, tries to print the whole entity, its fields, its references and Drupal's services behind them. It can run out of memory or take minutes. List the variable names first, then dump the one you want:

```twig
{{ dump(_context|keys) }}

{# Works even without debug mode on: #}
<pre>{{ _context|keys|join(', ') }}</pre>
```

The [Devel](https://www.drupal.org/project/devel) module adds `{{ devel_dump() }}` and `{{ devel_breakpoint() }}`, and Twig Tweak has [drupal_dump](#drupal_dump-and-dd).

### Breakpoints with Xdebug

Twig compiles each template into a PHP class, so you can step through it with Xdebug. Twig Tweak's `{{ drupal_breakpoint() }}` and Devel's `{{ devel_breakpoint() }}` stop at that point in the compiled template, with the template's variables available to inspect. Drupal's documentation says to set `twig.config: auto_reload: false` when you do this. The compiled templates live in `sites/default/files/php/twig/`.

### Clearing the Twig cache

Drupal caches compiled templates. If a new template doesn't show up, run `drush cr`. A new template file (rather than a change to an existing one) always needs a cache rebuild, even with "Disable Twig cache" ticked, because Drupal has to rediscover its templates.

### Common errors

- <strong>Nothing prints:</strong> a misspelt variable. Drupal stays silent about undefined variables.
- <strong>"Calling "X" method on a "Y" object is not allowed":</strong> the [sandbox](#the-twig-sandbox-in-drupal). Prepare the value in a preprocess function.
- <strong>"Object of type X cannot be printed":</strong> you've printed an object that can't become a string, often a field item list or entity. Print a property (`.value`, `.label`) or the render array from `content`.
- <strong>"Unknown "cache" tag" or "Unknown "spaceless" tag":</strong> a Symfony or Twig 1 and 2 example. See [cache](#-cache-) and [spaceless](#spaceless).
- <strong>"Unable to find template":</strong> for `@mytheme/...` paths, check the file is in the theme's `templates` folder or root and the theme is enabled. "Unable to find template "x.html.twig" in the Drupal theme registry" means you used a plain name that isn't a theme hook: add the `@mytheme/` namespace.
- <strong>HTML printed as text:</strong> you printed a raw value (such as `node.body.value`). Print the render array from `content` instead, rather than reaching for `|raw`.

## What not to do in Twig

Twig is for markup. The more logic you put in templates, the harder they are to read, test and keep fast. A few habits that will save future you:

- <strong>Don't fetch data in templates.</strong> Loading entities, running views or reading config belongs in a preprocess function or a block plugin, where you can add cache metadata and access checks. That includes Twig Tweak's `drupal_entity()` and `drupal_view()` (see below).
- <strong>Don't print raw values when a formatter will do.</strong> Raw values skip formatters, translation, access checks and cache metadata.
- <strong>Don't use `|raw` on anything that came from a person.</strong>
- <strong>Don't render the same thing twice.</strong> Store `|render` output in a variable.
- <strong>Don't remove `title_prefix` and `title_suffix`.</strong> Contextual links (the pencil icon) need them.
- <strong>Don't hard-code text.</strong> Use `t` or `trans`.
- <strong>Don't build huge templates.</strong> If a template is hundreds of lines long, split it into includes or components.
- <strong>Do move complex logic into preprocess.</strong> A preprocess function in your `.theme` file can work out a value once, add cache metadata and pass the template a simple variable to print.

## Drupal twig tweak

[Twig Tweak](https://www.drupal.org/project/twig_tweak) is a contributed module that adds a long list of Twig functions and filters, and over 220,000 sites use it. Version 3.4 is the stable release for Drupal 10.3 and 11, and version 4 is in alpha for Drupal 11.4 and later.

```shell
composer require drupal/twig_tweak
drush en twig_tweak
```

### A word of warning first

Twig Tweak is brilliant for a quick prototype, a one-off field you need in an odd place, or a site with no developer to write a preprocess function. Use it with your eyes open, though:

- <strong>It hides site building in templates.</strong> A view or block placed with `drupal_view()` or `drupal_block()` doesn't appear in the block layout, so site builders can't see it, move it or turn it off, and nothing warns you if someone deletes the view.
- <strong>It can repeat expensive work.</strong> Each call loads or builds things during rendering. Inside a loop, that's once per item: `drupal_entity()` in a loop of 50 teasers loads 50 entities. `drupal_view_result()` followed by `drupal_view()` runs the view twice.
- <strong>Some helpers drop cache metadata.</strong> `drupal_config()` returns the value only, so if you print the site name with it and someone changes the site name, cached pages keep showing the old one until the cache is cleared. Where a helper returns a render array (`drupal_view`, `drupal_entity`, `drupal_block` and others), the cache metadata comes with it.
- <strong>Debug helpers don't switch off.</strong> `dd()` and `drupal_dump()` dump on any site, debug mode or not, and `drupal_breakpoint()` stops Xdebug wherever it's running. Don't commit them.

If you find yourself using the same Twig Tweak call on several templates, that's a sign it belongs in a preprocess function, a block or a view mode.

### drupal_view

Embeds a view display: `drupal_view(view_name, display_id, ...arguments)`. The extra arguments go to the view's contextual filters.

```twig
{{ drupal_view('who_s_new', 'block_1') }}
{{ drupal_view('related_content', 'block_1', node.id) }}
```

### drupal_view_result

Returns a view's result rows (not printable) so you can count or test them:

```twig
{% if drupal_view_result('cart')|length == 0 %}
  {{ 'Your cart is empty.'|t }}
{% endif %}
```

Using it to decide whether to print the same view with `drupal_view()` runs the view twice. Views has a "No results behaviour" setting for that.

### drupal_block

Renders a block plugin by its plugin ID, with optional configuration. `wrapper=false` skips `block.html.twig`. Blocks rendered this way show their label unless you hide it:

```twig
{{ drupal_block('system_branding_block') }}
{{ drupal_block('system_branding_block', { label_display: false, use_site_name: false }) }}
{{ drupal_block('system_branding_block', wrapper=false) }}

{# For blocks that need a context, map it: #}
{{ drupal_block('plugin_id', { context_mapping: { node: '@node.node_route_context:node' } }) }}
```

To list the block plugin IDs on your site:

```shell
drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefinitions()));"
```

### drupal_region

Renders all the blocks placed in a region, optionally from another theme: `{{ drupal_region('sidebar_first') }}`, `{{ drupal_region('sidebar_first', 'olivero') }}`.

### drupal_entity

Renders an entity in a view mode, with access checks on by default: `drupal_entity(entity_type, id_or_uuid, view_mode, langcode, check_access)`.

```twig
{{ drupal_entity('block_content', 1) }}
{{ drupal_entity('node', 123, 'teaser') }}
{{ drupal_entity('block', 'olivero_branding', check_access=false) }}
```

### drupal_entity_form

Renders an entity's add or edit form: `drupal_entity_form(entity_type, id, form_mode, values, check_access)`.

```twig
{{ drupal_entity_form('node', 1) }}
{{ drupal_entity_form('node', values={ type: 'article' }) }}
{{ drupal_entity_form('user', NULL, 'register', check_access=false) }}
```

### drupal_field

Renders one field of an entity, using a view mode or your own display settings. It doesn't work with view modes that use Layout Builder.

```twig
{{ drupal_field('field_image', 'node', 1) }}
{{ drupal_field('field_image', 'node', 1, 'teaser') }}
{{ drupal_field('field_image', 'node', 1, { type: 'image_url', settings: { image_style: 'large' } }) }}
{{ drupal_field('field_image', 'node', 1, check_access=false) }}
```

### drupal_menu

Renders a menu, with optional starting level, depth and expansion: `drupal_menu(menu_name, level, depth, expand)`.

```twig
{{ drupal_menu('main') }}
{{ drupal_menu('main', 2, 1) }}      {# only the second level, one level deep #}
{{ drupal_menu('main', expand=true) }}
```

### drupal_form

Renders a form by its class name: `{{ drupal_form('Drupal\\search\\Form\\SearchBlockForm') }}`. Extra arguments go to the form's build method.

### drupal_image

Renders an image by file ID, UUID or URI, with an optional image style, attributes and responsive image style:

```twig
{{ drupal_image(123) }}
{{ drupal_image('public://ocean.jpg', 'thumbnail', { alt: 'The ocean'|t, loading: 'lazy' }) }}
{{ drupal_image('public://ocean.jpg', 'wide', responsive=true) }}
```

### drupal_token

Replaces a single token, with optional data and options:

```twig
{{ drupal_token('site:name') }}
{{ drupal_token('node:title', { node: node }) }}
{{ drupal_token('node:title', { node: node }, { clear: true }) }}
```

### drupal_config

Reads one value from a configuration object: `{{ drupal_config('system.site', 'name') }}`. See the warning above: it doesn't carry the config's cache tag, so cached pages won't update when the setting changes. For the site name and slogan, the "Site branding" block is the cache-friendly option.

### drupal_dump and dd

Dumps a variable, or every variable with no arguments, using Symfony's VarDumper: `{{ dd(node.label) }}`. Unlike core's `dump()`, it works whether debug mode is on or not. Handy, but easy to leave behind on a live site.

### drupal_title

Prints the current page's title, cached per URL: `{{ drupal_title() }}`.

### drupal_url and drupal_link

Build a URL or a link from a path as an editor would type it (`/node/1`, `node/1`, `?page=2`, `#anchor`) or an external URL, instead of a route name. With `check_access=true`, they return nothing if the visitor can't access the target:

```twig
{{ drupal_url('node/1', { query: { foo: 'bar' }, fragment: 'example', absolute: true }) }}
{{ drupal_link('View'|t, 'node/1', { attributes: { target: '_blank' } }) }}
{{ drupal_link('Administration'|t, '/admin', check_access=true) }}
```

Use core's [path, url](#path-and-url) and [link](#link) when you know the route name; use these when you start from a path.

### drupal_messages

Prints the status messages: `{{ drupal_messages() }}`.

### drupal_breadcrumb

Prints the breadcrumb: `{{ drupal_breadcrumb() }}`.

### drupal_breakpoint

Stops Xdebug at this point in the compiled template: `{{ drupal_breakpoint() }}`.

### drupal_contextual_links

Adds contextual links (the pencil icon) to something you've printed yourself. The wrapper needs the `contextual-region` class:

```twig
<div class="contextual-region">
  {{ drupal_contextual_links('entity.view.edit_form:view=frontpage:display_id=feed_1') }}
  {{ drupal_view('frontpage') }}
</div>
```

### token_replace

Replaces every token in a string or block of markup:

```twig
{{ '<h1>[site:name]</h1><div>[site:slogan]</div>'|token_replace }}

{% apply token_replace %}
  <h1>[site:name]</h1>
  <div>[site:slogan]</div>
{% endapply %}
```

### preg_replace

Replaces text with a regular expression: `{{ 'Drupal - community plumbing!'|preg_replace('/(Drupal)/', '<b>$1</b>') }}`. For simple replacements, Twig's own `replace` and `format` are faster and easier to read.

### image_style

Returns the URL of an image in an image style: `{{ 'public://images/ocean.jpg'|image_style('thumbnail') }}`. Check the URI isn't empty first, so you don't print a broken image:

```twig
{% set image_uri = node.field_image|file_uri %}
{% if image_uri is not null %}
  <div class="hero" style="background-image: url({{ image_uri|image_style('wide') }});"></div>
{% endif %}
```

### transliterate

Converts a string to plain ASCII: `{{ 'Привет!'|transliterate }}`. Handy for class names and file names.

### check_markup

Filters a string through a text format: `{{ node.field_notes.value|check_markup('basic_html') }}`. This is the safe way to print a raw text field's HTML if you really can't use `content`.

### format_size

Turns a number of bytes into a readable size: `{{ node.field_file.entity.filesize.value|format_size }}` gives something like "1.2 MB".

### truncate

Shortens a string to a number of characters, optionally at a word boundary and with an ellipsis: `truncate(max_length, wordsafe, add_ellipsis)`.

```twig
{{ node.body.summary|truncate(120, true, true) }}
```

### view

Renders an entity or a field in a view mode, or a field with display settings. Don't use it on the entity a template is already rendering (such as `node|view` inside `node.html.twig`): it renders itself forever.

```twig
{{ node.field_related.entity|view('teaser') }}
{{ node.field_image|view({ settings: { image_style: 'thumbnail' } }) }}
```

### with

The opposite of core's `without`: sets a property or child on a render array.

```twig
{{ content.field_image|with('#title', 'Photo'|t) }}
{{ content|with(['field_image', '#title'], 'Photo'|t) }}
```

### data_uri

Turns content into a data URI. Twig's HTML extension (in Drupal core from 11.4) has a filter with the same name and a similar job, so check the output if it surprises you.

```twig
<img src="{{ source('@mytheme/images/logo.svg')|data_uri('image/svg+xml') }}" alt="{{ 'Logo'|t }}">
```

### children

Returns a render array's children without its `#` properties, optionally sorted by weight. The easiest way to loop over a field's items in an entity template:

```twig
<ul>
  {% for tag in content.field_tags|children %}
    <li>{{ tag }}</li>
  {% endfor %}
</ul>
```

### file_uri and file_url

Get a file's URI or URL from a field, a field item or a file or media entity. They work with media fields too, including remote video (oEmbed), and `file_url(false)` gives an absolute URL:

```twig
{{ node.field_image|file_uri }}
{{ node.field_image[1]|file_url }}
{{ node.field_media|file_url }}
{{ node.field_document|file_url(false) }}
```

Core's `file_url()` is a function that only takes a URI string; Twig Tweak's `file_url` is a filter that also takes fields and entities. Same name, different jobs.

### entity_url

Returns an entity's URL object, for the canonical page or another link such as the edit form: `{{ node|entity_url }}`, `{{ node|entity_url('edit-form') }}`.

### entity_link

Returns a link to an entity, using its label or your own text: `{{ node|entity_link }}`, `{{ node|entity_link('Add new comment'|t, 'canonical', { fragment: 'comment-form' }) }}`.

### translation

Returns the entity in the current language (or a language you name), which you'll need when printing data from a referenced entity on a multilingual site: `{{ node.field_media.entity|translation.name.value }}`.

### cache_metadata

Extracts just the cache metadata from a render array or object and passes it up the page, so you can print raw values without stale caches:

```twig
<img src="{{ node.field_media|file_url }}" alt="">
{{ content.field_media|cache_metadata }}
```

### php

Runs PHP code. It's disabled unless you add `$settings['twig_tweak_enable_php_filter'] = TRUE;` to `settings.php`, and you should leave it disabled: PHP in templates is a security risk, and there's always a better way.

### New in Twig Tweak 4

The 4.0 alpha adds `drupal_logger()` to write to Drupal's log from a template, `{{ drupal_logger('mytheme', 'notice', 'Missing expected field') }}`, and a `token_replace_plain` filter that keeps replaced values as plain text, for use inside HTML attributes and meta tags. Never combine `token_replace_plain` with `|raw`.

## Why I wrote this

I've been writing Twig templates since early 2015, so more than eleven years now. In that time I've lost count of the number of times I've had the Twig docs, a handful of drupal.org pages and an old Stack Overflow answer open in different tabs, trying to remember whether something is a Twig thing, a Drupal thing or a Twig Tweak thing, and whether it still works in the version I'm using. I wanted one page that answers all of those questions in one go, for me as much as anyone else!

A lot of what's in here I learned the hard way: fields that are never empty, attributes that change in two places at once and caches that refuse to update. Writing it all down felt like the least I could do for whoever runs into those same walls next.

I hope it helps you, and saves you some of the head-scratching it cost me. Spotted something out of date, or got a favourite Twig trick I've missed? [Let me know](https://www.pivale.co/contact/) and I'll add it. And if your Drupal theme is fighting you more than it should, we can help with that too!

## Further reading

- [Twig documentation (3.x)](https://twig.symfony.com/doc/3.x/), including the full lists of [tags](https://twig.symfony.com/doc/3.x/tags/index.html), [filters](https://twig.symfony.com/doc/3.x/filters/index.html), [functions](https://twig.symfony.com/doc/3.x/functions/index.html) and [tests](https://twig.symfony.com/doc/3.x/tests/index.html). Some of them come from extra packages that Drupal doesn't include, so check the "installation" note at the top of a page.
- [Twig in Drupal](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal) on drupal.org, especially [Working with Twig templates](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/working-with-twig-templates), [Twig template naming conventions](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/twig-template-naming-conventions), [Filters](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/filters-modifying-variables-in-twig-templates), [Functions](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/functions-in-twig-templates) and [Debugging Twig templates](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/debugging-twig-templates).
- [Using single directory components](https://www.drupal.org/docs/develop/theming-drupal/using-single-directory-components) on drupal.org.
- The [Twig Tweak cheat sheet](https://git.drupalcode.org/project/twig_tweak/-/blob/4.x/docs/cheat-sheet.md).

---

Source: https://www.pivale.co/blog/twig-templates-drupal-cheat-sheet/. Pivale is a UK digital consultancy. Book a 30-minute call at https://www.pivale.co/contact/ or email hello@pivale.co.
