How to Work with Form Themes
This article explains how to use in your app any of the form themes provided by Symfony and how to create your own custom form theme.
Symfony Built-In Form Themes
Symfony comes with several built-in form themes that make your forms look great when using some of the most popular CSS frameworks. Each theme is defined in a single Twig template and they are enabled in the twig.form_themes option:
- form_div_layout.html.twig, wraps each form field inside a
<div>
element and it's the theme used by default in Symfony applications unless you configure it as explained later in this article. - form_table_layout.html.twig, wraps the entire form inside a
<table>
element and each form field inside a<tr>
element. - bootstrap_3_layout.html.twig, wraps each form field inside a
<div>
element with the appropriate CSS classes to apply the styles used by the Bootstrap 3 CSS framework. - bootstrap_3_horizontal_layout.html.twig, it's similar to the previous theme, but the CSS classes applied are the ones used to display the forms horizontally (i.e. the label and the widget in the same row).
- bootstrap_4_layout.html.twig, same as
bootstrap_3_layout.html.twig
, but updated for Bootstrap 4 CSS framework styles. - bootstrap_4_horizontal_layout.html.twig, same as
bootstrap_3_horizontal_layout.html.twig
but updated for Bootstrap 4 styles. - bootstrap_5_layout.html.twig, same as
bootstrap_4_layout.html.twig
, but updated for Bootstrap 5 CSS framework styles. - bootstrap_5_horizontal_layout.html.twig, same as
bootstrap_4_horizontal_layout.html.twig
but updated for Bootstrap 5 styles. - foundation_5_layout.html.twig, wraps each form field inside a
<div>
element with the appropriate CSS classes to apply the default styles of the version 5 of Foundation CSS framework. - foundation_6_layout.html.twig, wraps each form field inside a
<div>
element with the appropriate CSS classes to apply the default styles of the version 6 of Foundation CSS framework. - tailwind_2_layout.html.twig, wraps each form field inside a
<div>
element with the absolute minimum styles to make them usable. It is based on the Tailwind CSS form plugin.
Tip
Read the articles about Bootstrap 4 Symfony form theme and Bootstrap 5 Symfony form theme to learn more about them.
Applying Themes to all Forms
Symfony forms use by default the form_div_layout.html.twig
theme. If you
want to use another theme for all the forms of your app, configure it in the
twig.form_themes
option:
1 2 3 4
# config/packages/twig.yaml
twig:
form_themes: ['bootstrap_5_horizontal_layout.html.twig']
# ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!-- config/packages/twig.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:twig="http://symfony.com/schema/dic/twig"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd">
<twig:config>
<twig:form-theme>bootstrap_5_horizontal_layout.html.twig</twig:form-theme>
<!-- ... -->
</twig:config>
</container>
1 2 3 4 5 6 7 8 9 10
// config/packages/twig.php
use Symfony\Config\TwigConfig;
return static function (TwigConfig $twig): void {
$twig->formThemes([
'bootstrap_5_horizontal_layout.html.twig',
]);
// ...
};
You can pass multiple themes to this option because sometimes form themes only redefine a few elements. This way, if some theme doesn't override some element, Symfony looks up in the other themes.
The order of the themes in the twig.form_themes
option is important. Each
theme overrides all the previous themes, so you must put the most important
themes at the end of the list.
Applying Themes to Single Forms
Although most of the times you'll apply form themes globally, you may need to apply a theme only to some specific form. You can do that with the form_theme Twig tag:
1 2 3 4 5 6
{# this form theme will be applied only to the form of this template #}
{% form_theme form 'foundation_5_layout.html.twig' %}
{{ form_start(form) }}
{# ... #}
{{ form_end(form) }}
The first argument of the form_theme
tag (form
in this example) is the
name of the variable that stores the form view object. The second argument is
the path of the Twig template that defines the form theme.
Applying Multiple Themes to Single Forms
A form can also be customized by applying several themes. To do this, pass the
path of all the Twig templates as an array using the with
keyword (their
order is important, because each theme overrides all the previous ones):
1 2 3 4 5 6 7
{# apply multiple form themes but only to the form of this template #}
{% form_theme form with [
'foundation_5_layout.html.twig',
'form/my_custom_theme.html.twig'
] %}
{# ... #}
Applying Different Themes to Child Forms
You can also apply a form theme to a specific child of your form:
1
{% form_theme form.a_child_form 'form/my_custom_theme.html.twig' %}
This is useful when you want to have a custom theme for a nested form that's different from the one of your main form. Specify both your themes:
1 2
{% form_theme form 'form/my_custom_theme.html.twig' %}
{% form_theme form.a_child_form 'form/my_other_theme.html.twig' %}
Disabling Global Themes for Single Forms
Global form themes defined in the app are always applied to all forms, even
those which use the form_theme
tag to apply their own themes. You may want
to disable this for example when creating an admin interface for a bundle which
can be installed on different Symfony applications (and so you can't control what
themes are enabled globally). To do that, add the only
keyword after the list
of form themes:
1 2 3
{% form_theme form with ['foundation_5_layout.html.twig'] only %}
{# ... #}
Caution
When using the only
keyword, none of Symfony's built-in form themes
(form_div_layout.html.twig
, etc.) will be applied. In order to render
your forms correctly, you need to either provide a fully-featured form theme
yourself, or extend one of the built-in form themes with Twig's use
keyword instead of extends
to re-use the original theme contents.
1 2 3 4
{# templates/form/common.html.twig #}
{% use "form_div_layout.html.twig" %}
{# ... #}
Creating your Own Form Theme
Symfony uses Twig blocks to render each part of a form - field labels, errors,
<input>
text fields, <select>
tags, etc. A theme is a Twig template
with one or more of those blocks that you want to use when rendering a form.
Consider for example a form field that represents an integer property called
age
. If you add this to the template:
1
{{ form_widget(form.age) }}
The generated HTML content will be something like this (it will vary depending upon the form themes enabled in your app):
1
<input type="number" id="form_age" name="form[age]" required="required" value="33">
Symfony uses a Twig block called integer_widget
to render that field. This
is because the field type is integer
and you're rendering its widget
(as
opposed to its label
or errors
or help
). The first step to create a
form theme is to know which Twig block to override, as explained in the
following section.
Form Fragment Naming
The naming of form fragments varies depending on your needs:
- If you want to customize all fields of the same type (e.g. all
<textarea>
) use thefield-type_field-part
pattern (e.g.textarea_widget
). - If you want to customize only one specific field (e.g. the
<textarea>
used for thedescription
field of the form that edits products) use the_field-id_field-part
pattern (e.g._product_description_widget
).
In both cases, the field-part
can be any of these valid form field parts:
Fragment Naming for All Fields of the Same Type
These fragment names follow the type_part
pattern, where the type
corresponds to the field type being rendered (e.g. textarea
, checkbox
,
date
, etc) and the part
corresponds to what is being rendered (e.g.
label
, widget
, etc.)
A few examples of fragment names are:
form_row
- used by form_row() to render most fields;textarea_widget
- used by form_widget() to render atextarea
field type;form_errors
- used by form_errors() to render errors for a field;
Fragment Naming for Individual Fields
These fragment names follow the _id_part
pattern, where the id
corresponds to the field id
attribute (e.g. product_description
,
user_age
, etc) and the part
corresponds to what is being rendered
(e.g. label
, widget
, etc.)
The id
attribute contains both the form name and the field name (e.g.
product_price
). The form name can be set manually or generated automatically
based on your form type name (e.g. ProductType
equates to product
). If
you're not sure what your form name is, look at the HTML code rendered for your
form. You can also define this value explicitly with the block_name
option:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// ...
$builder->add('name', TextType::class, [
'block_name' => 'custom_name',
]);
}
In this example, the fragment name will be _product_custom_name_widget
instead of the default _product_name_widget
.
Custom Fragment Naming for Individual Fields
The block_prefix
option allows form fields to define their own custom
fragment name. This is mostly useful to customize some instances of the same
field without having to create a custom form type:
1 2 3 4 5 6 7 8 9
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('name', TextType::class, [
'block_prefix' => 'wrapped_text',
]);
}
Now you can use wrapped_text_row
, wrapped_text_widget
, etc. as the block
names.
Fragment Naming for Collections
When using a collection of forms, you have several ways of customizing the collection and each of its entries. First, use the following blocks to customize each part of all form collections:
1 2 3 4 5
{% block collection_row %} ... {% endblock %}
{% block collection_label %} ... {% endblock %}
{% block collection_widget %} ... {% endblock %}
{% block collection_help %} ... {% endblock %}
{% block collection_errors %} ... {% endblock %}
You can also customize each entry of all collections with the following blocks:
1 2 3 4 5
{% block collection_entry_row %} ... {% endblock %}
{% block collection_entry_label %} ... {% endblock %}
{% block collection_entry_widget %} ... {% endblock %}
{% block collection_entry_help %} ... {% endblock %}
{% block collection_entry_errors %} ... {% endblock %}
Finally, you can customize specific form collections instead of all of them.
For example, consider the following complex example where a TaskManagerType
has a collection of TaskListType
which in turn has a collection of
TaskType
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
class TaskManagerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = []): void
{
// ...
$builder->add('taskLists', CollectionType::class, [
'entry_type' => TaskListType::class,
'block_name' => 'task_lists',
]);
}
}
class TaskListType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = []): void
{
// ...
$builder->add('tasks', CollectionType::class, [
'entry_type' => TaskType::class,
]);
}
}
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = []): void
{
$builder->add('name');
// ...
}
}
Then you get all the following customizable blocks (where *
can be replaced
by row
, widget
, label
, or help
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
{% block _task_manager_task_lists_* %}
{# the collection field of TaskManager #}
{% endblock %}
{% block _task_manager_task_lists_entry_* %}
{# the inner TaskListType #}
{% endblock %}
{% block _task_manager_task_lists_entry_tasks_* %}
{# the collection field of TaskListType #}
{% endblock %}
{% block _task_manager_task_lists_entry_tasks_entry_* %}
{# the inner TaskType #}
{% endblock %}
{% block _task_manager_task_lists_entry_tasks_entry_name_* %}
{# the field of TaskType #}
{% endblock %}
Template Fragment Inheritance
Each field type has a parent type (e.g. the parent type of textarea
is
text
, and the parent type of text
is form
) and Symfony uses the
fragment for the parent type if the base fragment doesn't exist.
When Symfony renders for example the errors for a textarea type, it looks first
for a textarea_errors
fragment before falling back to the text_errors
and form_errors
fragments.
Tip
The "parent" type of each field type is available in the form type reference for each field type.
Creating a Form Theme in the same Template as the Form
This is recommended when doing customizations specific to a single form in your
app, such as changing all <textarea>
elements of a form or customizing a
very special form field which will be handled with JavaScript.
You only need to add the special {% form_theme form _self %}
tag to the same
template where the form is rendered. This causes Twig to look inside the template
for any overridden form blocks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
{% extends 'base.html.twig' %}
{% form_theme form _self %}
{# this overrides the widget of any field of type integer, but only in the
forms rendered inside this template #}
{% block integer_widget %}
<div class="...">
{# ... render the HTML element to display this field ... #}
</div>
{% endblock %}
{# this overrides the entire row of the field whose "id" = "product_stock" (and whose
"name" = "product[stock]") but only in the forms rendered inside this template #}
{% block _product_stock_row %}
<div class="..." id="...">
{# ... render the entire field contents, including its errors ... #}
</div>
{% endblock %}
{# ... render the form ... #}
The main disadvantage of this method is that it only works if your template
extends another ('base.html.twig'
in the previous example). If your template
does not, you must point form_theme
to a separate template, as explained in
the next section.
Another disadvantage is that the customized form blocks can't be reused when rendering other forms in other templates. If that's what you need, create a form theme in a separate template as explained in the next section.
Creating a Form Theme in a Separate Template
This is recommended when creating form themes that are used in your entire app or even reused in different Symfony applications. You only need to create a Twig template somewhere and follow the form fragment naming rules to know which Twig blocks to define.
For example, if your form theme is simple and you only want to override the
<input type="integer">
elements, create this template:
1 2 3 4 5 6
{# templates/form/my_theme.html.twig #}
{% block integer_widget %}
{# ... add all the HTML, CSS and JavaScript needed to render this field #}
{% endblock %}
Now you need to tell Symfony to use this form theme instead of (or in addition
to) the default theme. As explained in the previous sections of this article, if
you want to apply the theme globally to all forms, define the
twig.form_themes
option:
1 2 3 4
# config/packages/twig.yaml
twig:
form_themes: ['form/my_theme.html.twig']
# ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!-- config/packages/twig.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:twig="http://symfony.com/schema/dic/twig"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd">
<twig:config>
<twig:form-theme>form/my_theme.html.twig</twig:form-theme>
<!-- ... -->
</twig:config>
</container>
1 2 3 4 5 6 7 8 9 10
// config/packages/twig.php
use Symfony\Config\TwigConfig;
return static function (TwigConfig $twig): void {
$twig->formThemes([
'form/my_theme.html.twig',
]);
// ...
};
If you only want to apply it to some specific forms, use the form_theme
tag:
1 2 3 4 5
{% form_theme form 'form/my_theme.html.twig' %}
{{ form_start(form) }}
{# ... #}
{{ form_end(form) }}
Reusing Parts of a Built-In Form Theme
Creating a complete form theme takes a lot of work because there are too many different form field types. Instead of defining all those Twig blocks, you can define only the blocks you are interested in and then configure multiple form themes in your app or template. This works because when rendering a block which is not overridden in your custom theme, Symfony falls back to the other themes.
Another solution is to make your form theme template extend from one of the
built-in themes using the Twig "use" tag instead of the extends
tag so
you can inherit all its blocks (if you are unsure, extend from the default
form_div_layout.html.twig
theme):
1 2 3 4
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}
{# ... override only the blocks you are interested in #}
Finally, you can also use the Twig parent() function to reuse the original content of the built-in theme. This is useful when you only want to make minor changes, such as wrapping the generated HTML with some element:
1 2 3 4 5 6 7 8
{# templates/form/my_theme.html.twig #}
{% use 'form_div_layout.html.twig' %}
{% block integer_widget %}
<div class="some-custom-class">
{{ parent() }}
</div>
{% endblock %}
This technique also works when defining the form theme in the same template that renders the form. However, importing the blocks from the built-in themes is a bit more complicated:
1 2 3 4 5 6 7 8 9 10 11 12 13
{% form_theme form _self %}
{# import a block from the built-in theme and rename it so it doesn't
conflict with the same block defined in this template #}
{% use 'form_div_layout.html.twig' with integer_widget as base_integer_widget %}
{% block integer_widget %}
<div class="some-custom-class">
{{ block('base_integer_widget') }}
</div>
{% endblock %}
{# ... render the form ... #}
Customizing the Form Validation Errors
If you define validation rules for your objects, you'll see
some validation error messages when the submitted data is not valid. These
messages are displayed with the form_errors()
function and can be customized with the form_errors
Twig block in any form
theme, as explained in the previous sections.
An important thing to consider is that certain errors are associated to the
entire form instead of a specific field. In order to differentiate between
global and local errors, use one of the
variables available in forms called
compound
. If it is true
, it means that what's being currently rendered
is a collection of fields (e.g. a whole form), and not just an individual field:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
{# templates/form/my_theme.html.twig #}
{% block form_errors %}
{% if errors|length > 0 %}
{% if compound %}
{# ... display the global form errors #}
<ul>
{% for error in errors %}
<li>{{ error.message }}</li>
{% endfor %}
</ul>
{% else %}
{# ... display the errors for a single field #}
{% endif %}
{% endif %}
{% endblock form_errors %}