Includes, Embeds and Components
Four ways to reuse markup - include, embed, macros and Twig Components - and the question that picks between them.
{{ include('_card.html.twig', {article: article}) }} <twig:Alert message="..." />Inheritance shares a page shell downwards. Reuse goes sideways: the same card, alert or form row appearing in several unrelated templates. Twig offers four tools, and the choice comes down to what the fragment needs.
include, for a fragment
{{ include('article/_card.html.twig', {article: article}) }} renders another template in place. By default the fragment also sees the caller's variables; only on the tag form, or with_context: false, cuts that off, which makes the fragment's inputs explicit. The tag form {% include 'article/_card.html.twig' %} and the function form above do the same work; the function can be piped, as in {{ include(tpl)|trim }}. Naming partials with a leading underscore is the usual convention.
embed, when the fragment has holes
{% embed %} is include plus inheritance: it renders another template and lets the calling site override that template's blocks. Reach for it for a skeleton such as a panel or a modal, where the frame is fixed and the contents differ.
Macros, for a markup function
A macro is a named, parameterised snippet, defined with {% macro %} and brought in with {% import %} or {% from %}. It is the right shape for a form field or a badge called many times with different arguments.
Twig Components, when logic is involved
A component is a PHP class paired with a template. Install symfony/ux-twig-component, mark the class #[AsTwigComponent], and put its template at templates/components/<Name>.html.twig. Public properties become props set from the calling template; public methods are callable on this. The HTML form <twig:Alert message="..." /> and the function form {{ component('Alert', {message: '...'}) }} are the same render.
Example
// src/Twig/Components/Alert.php
declare(strict_types=1);
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
// The namespace App\Twig\Components\ is mapped to templates/components/
// by config/packages/twig_component.yaml, so this class is paired with
// templates/components/Alert.html.twig.
#[AsTwigComponent]
final class Alert
{
// Public properties are the props. These two defaults apply when the
// calling template does not pass them.
public string $type = 'info';
public string $message = '';
// Public methods are callable from the component template as
// {{ this.cssClass }} - logic that would be awkward in Twig.
public function cssClass(): string
{
return 'alert alert-' . ($this->type === 'danger' ? 'danger' : 'info');
}
}When to use it
- Render the same article card on a list page, a search page and a sidebar.
- Wrap varying content in a fixed panel or modal frame.
- Build a reusable alert that derives its CSS classes from a severity prop.
More examples
include and embed side by side
Use include when the fragment is self-contained and embed when the fragment is a frame with holes in it. Adding only is worth the extra typing on a partial used in several places, because it turns an accidental dependency on a caller's variable into an immediate error.
{# templates/article/index.html.twig #}
{# Tag form and function form render the same fragment. #}
{% include 'article/_card.html.twig' with {article: articles|first} %}
{{ include('article/_card.html.twig', {article: articles|first}) }}
{# only: the fragment sees nothing except what is passed here. #}
{% include 'article/_card.html.twig' with {article: articles|first} only %}
{# embed: the panel owns the frame, this template fills its blocks. #}
{% embed 'layout/_panel.html.twig' with {title: 'Latest'} %}
{% block body %}
<p>{{ articles|length }} articles published.</p>
{% endblock %}
{% endembed %}A macro and the two ways to import it
Macros take default argument values, which keeps the common call short. The important difference from include is isolation: user_email has to be passed in, because the macro cannot reach the template's variables. Use macros for small markup called many times with varying arguments.
{# templates/_macros/form.html.twig #}
{% macro field(name, label, value = '', type = 'text') %}
<label for="{{ name }}">{{ label }}</label>
<input id="{{ name }}" type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{# templates/account/edit.html.twig #}
{# Whole file under an alias. #}
{% import '_macros/form.html.twig' as form %}
{{ form.field('email', 'Email address', user_email, 'email') }}
{# Or one macro by name. #}
{% from '_macros/form.html.twig' import field %}
{{ field('city', 'City') }}The component template and both call forms
The HTML form reads better in a page full of markup and is what you will see in recent projects; the function form is useful when the component name or the props come from variables. Choose a component over a macro as soon as the fragment needs PHP to decide anything.
{# templates/components/Alert.html.twig #}
{# Props are plain variables; this is the component object itself. #}
<div class="{{ this.cssClass }}" role="alert">
{{ message }}
</div>
{# templates/checkout/failed.html.twig #}
{# HTML form. #}
<twig:Alert type="danger" message="Payment failed." />
{# Function form - the same render, written as a call. #}
{{ component('Alert', {type: 'danger', message: 'Payment failed.'}) }}
{# type is omitted, so the class default 'info' applies. #}
<twig:Alert message="Your cart was saved." />
Discussion