Twig in Drupal
Every piece of HTML Drupal outputs comes from a Twig template, and every template can be replaced by your theme. This page shows you how to find the template behind any markup, override it, and use the filters and functions Drupal adds on top of Twig 3.
Work confidently in any *.html.twig file: turn on template debugging, override the template you need (including Views templates), and use Drupal’s Twig extensions instead of re-implementing them. If you want to understand how render arrays become templates in the first place, read Render API and the theming pipeline first.
Prerequisites
Section titled “Prerequisites”- A Drupal 11 site you can edit code on, per Set up your environment.
- A custom theme. The first theme tutorial gets you one in a few minutes.
- Drupal 11 ships Twig 3 (core requires
twig/twig^3.27). Twig’s own syntax is documented in the Twig documentation; this page covers what Drupal adds and changes.
Turn on Twig development mode
Section titled “Turn on Twig development mode”Go to Administer → Configuration → Development → Development settings/admin/config/development/settings and enable Twig development mode. This turns on debug output and template auto-reloading; on the same page you can also disable render caching while you work. Drupal stores these toggles outside configuration (in a key-value store), so they cannot leak into a config export and onto production.
With Twig development mode on, every rendered template is wrapped in HTML comments in the page source:
<!-- THEME DEBUG --><!-- THEME HOOK: 'node' --><!-- FILE NAME SUGGESTIONS: ▪️ node--1--teaser.html.twig ▪️ node--1.html.twig ▪️ node--article--teaser.html.twig ▪️ node--article.html.twig ✅ node--teaser.html.twig ▪️ node.html.twig--><!-- 💡 BEGIN CUSTOM TEMPLATE OUTPUT from 'themes/custom/first_theme/templates/node--teaser.html.twig' -->The list shows every file name Drupal looked for, from most to least specific. The check mark marks the template in use; the 💡 BEGIN CUSTOM TEMPLATE OUTPUT marker appears when that file comes from your active theme rather than a module.
Override a template
Section titled “Override a template”Copy the template file Drupal is currently using into your theme’s templates/ directory, keep the file name, and rebuild caches with ddev drush cache:rebuild (ddev drush cr). Drupal scans for template files only when the theme registry is rebuilt, so a cache rebuild is required once per new file (after that, edits to the file are picked up automatically in development mode).
A trimmed-down override of the node template looks like this:
{#/** * @file * First theme's override of the node template. * * Drupal finds this file by name: node.html.twig in the active theme wins * over the copy that ships with the node module. * * Available variables include: * - node: The node entity. * - label: The (possibly hidden) node title. * - content: The render array of all visible fields. Render it whole with * {{ content }} or pick fields with {{ content.field_name }}. * - attributes / title_attributes / content_attributes: HTML attribute * objects for the wrapper, title and content elements. * - page: TRUE when the node is shown as a full page. * * @see template_preprocess_node() */#}<article{{ attributes.addClass('node', 'node--' ~ node.bundle|clean_class) }}> {{ title_prefix }} {% if label and not page %} <h2{{ title_attributes }}> <a href="{{ url }}" rel="bookmark">{{ label }}</a> </h2> {% endif %} {{ title_suffix }}
<div{{ content_attributes.addClass('node__content') }}> {{ content }} </div></article>To target a narrower situation than the base template, use one of the suggested file names from the debug output instead, such as node--teaser.html.twig for teasers or node--article.html.twig for a content type. The most specific existing file always wins.
The variables available in a template are listed in the documentation block of the original template you copied. To inspect one at runtime, use {{ dump(label) }}. Drupal replaces Twig’s dump() with Symfony VarDumper output when the symfony/var-dumper package is installed, which makes dumping attributes or content survivable; plain dump() without VarDumper can exhaust memory on large variables.
Use Drupal’s Twig filters
Section titled “Use Drupal’s Twig filters”Drupal registers its filters in \Drupal\Core\Template\TwigExtension. The ones you will use in practice:
| Filter | What it does |
|---|---|
t, trans | Runs the string through Drupal’s translation system. Only apply it to literal strings you author in the template, never to user data. |
clean_class | Converts a string into a valid, predictable CSS class name (Foo Bar! becomes foo-bar). |
clean_id | Converts a string into a valid HTML id attribute value. |
clean_unique_id | Like clean_id, but guarantees uniqueness for the current response. |
add_class | Adds classes to a render array’s wrapper attributes and returns the modified array. |
set_attribute | Sets one attribute on a render array and returns the modified array. |
without | Returns a copy of a render array with the named children removed, leaving the original intact. |
render | Forces a render array or object to a string. Printing with {{ }} already renders, so you mostly need this in {% set %} or conditions. |
format_date | Formats a timestamp with a configured date format, through the date formatter service. |
add_suggestion | Appends a new highest-priority theme hook suggestion to an element (see below). |
safe_join | Joins a list into a string while escaping each unsafe item. |
Examples of the ones with non-obvious syntax:
{# Print all fields except links, to print links separately later. #}{{ content|without('links') }}{# Add a class to a field's wrapper without a preprocess function. #}{{ content.field_tags|add_class('tags', 'tags--inline') }}{# Render to a string to test emptiness reliably. #}{% if content.field_summary|render|striptags|trim %} <div class="summary">{{ content.field_summary }}</div>{% endif %}Two more filters exist for internal use: placeholder (used by the {% trans %} tag) and drupal_escape (Drupal’s replacement for Twig’s auto-escaping). You do not call them yourself.
Use Drupal’s Twig functions
Section titled “Use Drupal’s Twig functions”| Function | What it does |
|---|---|
path | Generates a relative URL path from a route name: path('entity.node.canonical', {'node': node.id}). |
url | Like path, but absolute. |
link | Builds a full anchor element from text, a URL or \Drupal\Core\Url object, and optional attributes. |
file_url | Turns a file URI such as public://logo.svg into a root-relative URL. |
attach_library | Attaches an asset library to the response: {{ attach_library('first_theme/slider') }}. |
active_theme | Returns the active theme’s machine name. |
active_theme_path | Returns the active theme’s directory path. |
create_attribute | Creates an \Drupal\Core\Template\Attribute object so your own markup can use the same attributes-style API. |
icon | Returns a render array for an icon from an icon pack (the Icon API). |
create_attribute is the one to remember when writing your own wrappers:
{% set wrapper = create_attribute({'class': ['banner'], 'role': 'banner'}) %}<div{{ wrapper.setAttribute('id', heading|clean_id) }}>A function named render_var also exists; Drupal wraps every {{ }} print statement with it automatically so render arrays print correctly. Never call it directly.
Add suggestions instead of inline logic
Section titled “Add suggestions instead of inline logic”When a template starts accumulating if branches for one special case, give that case its own template. The add_suggestion filter creates a new theme hook suggestion from inside Twig:
{{ content.field_topics|add_suggestion('pills') }}This renders the field with field--pills.html.twig if your theme provides it, falling back to the normal field templates if not. The filter also adds a render cache key, so cached markup from before the suggestion does not leak in.
Modules and themes can add suggestions from PHP too, with hook_theme_suggestions_HOOK() and hook_theme_suggestions_HOOK_alter(). Use PHP when the suggestion depends on data Twig does not have; use add_suggestion when it is purely presentational.
Override Views templates
Section titled “Override Views templates”Views output is rendered through three layers of templates, and each layer accepts suggestions built from the view name, the display ID, and the display type:
views-view.html.twigwraps the whole view: title, header, rows, pager, footer.- A style template renders the row collection; the default style is
views-view-unformatted.html.twig. - A row template renders one row; the default row style is
views-view-fields.html.twig.
For a view named frontpage with a page display whose ID is page_1, Drupal tries these wrapper templates in order:
views-view--frontpage--page-1.html.twigviews-view--page-1.html.twigviews-view--default.html.twigviews-view--frontpage--page.html.twigviews-view--page.html.twigviews-view--frontpage.html.twigviews-view.html.twigviews-view--default comes from the view’s tag (default for the core front page view): each tag adds one suggestion. The --page entries name the display plugin, and appear because the display ID differs from it.
The same pattern applies to the style and row templates (views-view-unformatted--frontpage.html.twig, and so on). You do not need to memorize the matrix: with Twig development mode on, the FILE NAME SUGGESTIONS comment above any view lists every accepted name. Copy the template you want to specialize from core/modules/views/templates/ into your theme, rename it to the suggestion you want, and run ddev drush cr.
Fields inside a view row are regular field output, so node and field template overrides apply to them as usual. The full template-naming rules are documented in the Views template files topic.
Verify
Section titled “Verify”- With Twig development mode enabled, load the page you themed and view the page source.
- Find the
THEME HOOKcomment for your element. The suggestion list must show your file with the check mark, and the surrounding markers must readBEGIN CUSTOM TEMPLATE OUTPUTwith your theme’s path. - If your file is listed but not selected, a more specific template elsewhere is winning; if it is not listed at all, the file name is not a valid suggestion for that hook (watch for an
INVALID FILE NAME SUGGESTIONScomment).
Variations
Section titled “Variations”Compute variables in preprocess instead of Twig
Section titled “Compute variables in preprocess instead of Twig”When a template needs data that requires real logic (loading config, formatting, querying), compute it in a preprocess hook with hook_preprocess_HOOK() and hand the template a finished variable. Twig stays declarative; PHP stays testable. The first theme tutorial part 2 walks through this split.
Reuse markup with single-directory components
Section titled “Reuse markup with single-directory components”If you are overriding templates mainly to repeat the same markup pattern, define it once as a component and embed it everywhere it appears. See Single-directory components.
Related
Section titled “Related”- Render API and the theming pipeline explains where template variables come from.
- Build your first theme 2: Twig templates and suggestions practices overrides end to end.
- Asset libraries covers what
attach_libraryattaches. The theme system overview topicdocuments the full suggestion and preprocess order.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.