Who this is for. The developer building a Twig site's page templates, partials and component library. Afterwards you can choose between an include, an embed and a macro for each reusable piece, and carry state across templates that do not share variables.
Twig inheritance, includes, embed and macros
How Twig templates are composed on Core dna: a base template and the blocks a child fills, includes with with and only, embed for parameterised wrappers, macros and import, the template context for state that crosses templates, and whitespace control, with the Smarty equivalent where one exists.
<!doctype html>
<html lang="en">
<head>
<title>{% block title %}{{ page.getName()|e }}{% endblock %} | Example</title>
{% block head %}{% endblock %}
</head>
<body class="{{ bodyClass|default('') }}">
{{ include('includes/nav.html') }}
{% if block('promo') is defined %}<aside class="promo">{{ block('promo') }}</aside>{% endif %}
<main>{% block content %}{% endblock %}</main>
<footer>{% block footer %}<nav>{% block footer_links %}{% endblock %}</nav>{% endblock %}</footer>
</body>
</html>
{% extends 'includes/base.html' %}
{% set bodyClass = 'article' %} {# outside any block: reaches the base everywhere #}
{% block title %}Article: {{ parent() }}{% endblock %}
{% block promo %}Read the full series{% endblock %}
{% block content %}
<h1>{{ page.getName()|e }}</h1>
{{ page.body }}
{% endblock %}
{% block footer_links %}{{ parent() }}<a href="/archive">Archive</a>{% endblock %}
{{ parent() }}prints the base's content of the block, wherever you place it: before your content for Smarty'sappend, after it forprepend.- Nested blocks can be overridden one at a time: the child above replaces
footer_linksand keeps the rest offooter. block('promo') is definedrenders a wrapper only when a child fills the block, the job Smarty'shidedoes.- Anything but blocks and
setat a child's top level is an error:A template that extends another one cannot include content outside Twig blocks. Smarty discards the same content. - A
setoutside any block reaches the base everywhere; asetinside a block stays in that block. The platform'ssetalso stores the value in the template context, which is how a value set inside a block reaches the rest of the page (below).
include, with and only
An include renders another template in place. By default it sees all of the caller's variables, plus any you pass with with; only gives it nothing but what you pass. That isolation is the main thing Twig's include has over Smarty's.
{% include 'elements/card.html' with {title: post.getName(), url: post.getFullPath()} %}
{% include 'elements/card.html' with {title: post.getName(), url: post.getFullPath()} only %}
{# the function form, which can be filtered or assigned #}
{{ include('elements/card.html', {title: post.getName()}, with_context = false) }}
{% set cardHtml = include('elements/card.html', {title: post.getName()}) %}
{# an optional partial #}
{% include 'elements/' ~ variant ~ '.html' ignore missing %}
- Use
onlyfor shared partials. A partial that receives only its parameters behaves the same wherever it is included, and cannot pick up an unrelated variable of the same name from its caller. - The underscore helpers survive
only. For a page render they are Twig globals, so an isolated partial still has_request,_url,__and the rest. - Variables set inside the include stay there, as in Smarty; their stored copies are in the template context.
ignore missingturns a missing file into nothing rather than an error, which Smarty cannot do.
Shared partials go in includes/ for chrome, elements/ for small reusable pieces, and a partials/ folder beside the templates that share them, always named in full from templates/: Twig has no relative names.
embed: an include whose blocks you fill
embed includes a template and overrides its blocks in one step, which suits wrappers such as cards, panels and modals: the wrapper owns the markup, the caller supplies the parts. Smarty has no equivalent; there you pass captured markup into an include.
{# templates/elements/panel.html #}
<div class="panel panel--{{ tone|default('plain') }}">
<h3 class="panel__title">{% block title %}{% endblock %}</h3>
<div class="panel__body">{% block body %}{% endblock %}</div>
</div>
{# a page template #}
{% embed 'elements/panel.html' with {tone: 'info'} %}
{% block title %}Opening hours{% endblock %}
{% block body %}{{ component.hours }}{% endblock %}
{% endembed %}
embed takes with and only like an include, and the blocks inside it see the caller's variables.
Macros and import
A macro is a named fragment that takes arguments and returns markup. Macros live in a library file and are imported under a name, or used from the same file through _self.
{# templates/includes/ui.twig #}
{% macro badge(label, tone = 'info') %}
<span class="badge badge--{{ tone }}">{{ label|e }}</span>
{% endmacro %}
{# any template #}
{% import 'includes/ui.twig' as ui %}
{{ ui.badge('New') }}
{{ ui.badge('Sold out', 'warning') }}
{% from 'includes/ui.twig' import badge %}
{{ badge('Last few') }}
- A macro does not see the calling template's variables. Everything it needs comes in as arguments. The exceptions are the underscore helpers, which are globals, and the template context.
- A macro beats an include when the piece is a function of a few values and is used many times on a page, such as a badge, a price or a formatted date: the arguments are explicit and the call is short. Prefer an include for a larger piece with its own logic, and an embed when the caller fills slots.
- Every
setinside a macro is stored in the template context, so give working names a prefix (The platform's set tag).
Macros are also where a Twig site keeps reusable transformations it would otherwise write as a filter, since a site cannot register extensions.
The template context in Twig
_templateContext works in Twig as in Smarty: one store for the whole render, which components and records rendered with getHtml() share, and which crosses every block, include and macro boundary. Read with _templateContext.get('key', default); write with the platform's set, which stores automatically, or explicitly with {% do _templateContext.set('key', value) %}. The heading-level pattern, in Twig:
{% set _section_level = min(_templateContext.get('headingLevel', 2), 6) %}
<section>
<h{{ _section_level }}>{{ component.getAreaText('heading')|e }}</h{{ _section_level }}>
{% do _templateContext.set('headingLevel', _section_level + 1) %}
{% for element in component.getAreaCollectionElements('items') %}
{% set _section_child = element.isGlobal() ? element.getGlobalComponent() : element.getComponent() %}
{{ _section_child ? _section_child.getHtml() }}
{% endfor %}
{% do _templateContext.set('headingLevel', _section_level) %}
</section>
The explicit set() calls matter here: the key, headingLevel, is not the name of any variable, and the last call restores the level for the next section. Pass a variable across an inheritance boundary covers the inheritance case in both engines.
Whitespace control
A - inside a delimiter removes all whitespace on that side: {%- if x %}, {{- value -}}. A ~ removes spaces and tabs but keeps line breaks. This is the Twig way to keep generated markup tight, the job Smarty does with {strip} around a whole block. The spaceless filter, used as {% apply spaceless %}, still works but is deprecated since Twig 3.12; prefer the modifiers. As with {strip}, removing whitespace between inline elements or words joins them on the page.
Was this page helpful?
Your answer goes to the docs team.