09/02/2026
Rendering a Symfony form field by field in Twig
{{ form(form) }} renders everything and gives you no say in any of it.
The moment the design needs two fields side by side, or a class on one
input, you have to take it apart:
{{ form_start(form) }}
<div class="row">
{{ form_row(form.firstName) }}
{{ form_row(form.lastName) }}
</div>
{{ form_row(form.email, {'attr': {'class': 'input-wide'}}) }}
{{ form_rest(form) }}
<button type="submit">Save</button>
{{ form_end(form) }}
What each one does
form_start opens the <form> tag with the right method and action,
and sets the enctype when the form has a file upload. Writing the tag by
hand is where multipart uploads quietly stop working.
form_row is label, widget and errors together, wrapped in the
theme's row markup. It is the one to reach for by default.
form_rest renders every field you have not rendered yet. This is
the important one and it is the line people delete because "there is
nothing left". It is also what renders the CSRF token, so a form without
it either loses its protection or fails validation with no visible reason.
form_end closes the tag and, unless you pass
{'render_rest': false}, also does form_rest for you.
Going below form_row
When the row markup itself is in the way, the three pieces are separate:
<div class="field">
{{ form_label(form.email, 'Work email') }}
{{ form_widget(form.email, {'attr': {'placeholder': 'you@example.com'}}) }}
{{ form_errors(form.email) }}
</div>
Two things worth knowing about that:
form_errors(form)with the form itself, rather than a field, renders the errors that belong to the whole form rather than to any one input. That includes anything a validator attached at class level, and errors you added from a controller against no particular field. Leave it out and those never appear anywhere.form_labeltakes the label as its second argument, but so does the form type'slabeloption. Setting it in the type keeps the template free of copy, which matters as soon as anything is translated.
Reading a field's own values
Every field exposes a vars array, which is how you reach the things the
functions above do not print:
{{ form.email.vars.id }} {# the rendered id attribute #}
{{ form.email.vars.required }} {# true or false #}
{{ form.email.vars.value }} {# what is currently in it #}
{% if not form.email.vars.valid %}
<span class="has-error">Check this field</span>
{% endif %}
vars is also where the collection prototype lives, which is its own
piece of work — see the dynamic collection article for that.
When to customize the theme instead
If you are writing the same wrapper markup around every field on every
form, that belongs in a form theme rather than in each template. A theme
is a Twig file of blocks that override how rows, widgets and errors are
drawn everywhere at once, and {% form_theme form 'form/fields.html.twig' %}
applies it.
The rule of thumb: field-by-field rendering for a layout that is genuinely one-off, a form theme for anything the whole site does.