Skip to content

Field

component

A form field: its label, its control, its help and its errors, together; and sets of fields under a legend.

Preview

Light

The address we write to.

Code
<twig:Field :invalid="true">
    <twig:Field:Label for="email">Email address</twig:Field:Label>
    <twig:Input id="email" type="email" name="email" aria-describedby="email-help email-error" aria-invalid="true" />
    <twig:Field:Description id="email-help">The address we write to.</twig:Field:Description>
    <twig:Field:Error id="email-error" :errors="['This address is already taken.']" />
</twig:Field>

Dark

The address we write to.

Code
<twig:Field :invalid="true">
    <twig:Field:Label for="email">Email address</twig:Field:Label>
    <twig:Input id="email" type="email" name="email" aria-describedby="email-help email-error" aria-invalid="true" />
    <twig:Field:Description id="email-help">The address we write to.</twig:Field:Description>
    <twig:Field:Error id="email-error" :errors="['This address is already taken.']" />
</twig:Field>

Try it

Change the props; the example is rendered again by the server.

The address we write to.

<twig:Field :invalid="true">
    <twig:Field:Label for="email">Email address</twig:Field:Label>
    <twig:Input id="email" type="email" name="email" aria-describedby="email-help email-error" aria-invalid="true" />
    <twig:Field:Description id="email-help">The address we write to.</twig:Field:Description>
    <twig:Field:Error id="email-error" :errors="['This address is already taken.']" />
</twig:Field>

Variants

orientation

How the label and the control are laid out; responsive goes horizontal on a wide container.

The address we write to.

The address we write to.

orientation="vertical"

The address we write to.

The address we write to.

orientation="horizontal"

The address we write to.

The address we write to.

orientation="responsive"

invalid

Whether the field holds an invalid value: its error shows and its colours say so.

The address we write to.

The address we write to.

invalid="false"

The address we write to.

The address we write to.

invalid="true"

Installation

php bin/console ux:install field --kit=github.com/parler-haut-interagir-librement/ux-kit

Props

PropTypeDefaultDescription
orientation 'vertical'|'horizontal'|'responsive' 'vertical' How the label and the control are laid out; responsive goes horizontal on a wide container.
invalid bool false Whether the field holds an invalid value: its error shows and its colours say so.

Accessibility

The label names the control through for; the description and the error are tied to it by aria-describedby and aria-invalid, which the application writes on the control. The error is a live alert, written only when there is one. A Field:Set is a fieldset, its Field:Legend the name of the group.

Files to copy

templates/components/Field.html.twig
{%- props
    ## 'vertical'|'horizontal'|'responsive' How the label and the control are laid out; responsive goes horizontal on a wide container.
    orientation = 'vertical',
    ## bool Whether the field holds an invalid value: its error shows and its colours say so.
    invalid = false
-%}
{%- set _orientation = orientation in ['vertical', 'horizontal', 'responsive'] ? orientation : 'vertical' -%}
<div role="group" data-slot="field" data-orientation="{{ _orientation }}"{% if invalid %} data-invalid="true"{% endif %}{{ attributes.defaults({class: 'isField' ~ ({horizontal: ' isHorizontal', responsive: ' isResponsive'}[_orientation] ?? '') ~ (invalid ? ' isDanger' : '')}) }}>
{##- A `Field:Label`, the control, and optionally a `Field:Description` and a `Field:Error`. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/Field/Content.html.twig
<div data-slot="field-content"{{ attributes.defaults({class: 'isFieldContent'}) }}>
{##- The label and the description of a horizontal field, kept together beside its control. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/Field/Description.html.twig
<p data-slot="field-description"{{ attributes.defaults({class: 'isFieldDescription'}) }}>
{##- A help text, which the control names with `aria-describedby`. -#}
{%- block content %}{% endblock -%}
</p>
templates/components/Field/Error.html.twig
{#-
 # The errors of a field: a list when there are several, a paragraph for one, nothing for none. With content in its
 # block, the block is written instead.
 #}
{%- props
    ## list<string> The error messages, each escaped.
    errors = []
-%}
{%- set _content -%}
{##- An error written by hand, in place of `errors`. -#}
{%- block content %}{% endblock -%}
{%- endset -%}
{%- if _content|trim is not empty or errors is not empty -%}
<div role="alert" data-slot="field-error"{{ attributes.defaults({class: 'isFieldError'}) }}>
{%- if _content|trim is not empty -%}
{{ _content }}
{%- elseif errors|length == 1 -%}
<p>{{ errors|first }}</p>
{%- else -%}
<ul>{% for _error in errors %}<li>{{ _error }}</li>{% endfor %}</ul>
{%- endif -%}
</div>
{%- endif -%}
templates/components/Field/Group.html.twig
<div data-slot="field-group"{{ attributes.defaults({class: 'isFieldGroup'}) }}>
{##- Fields stacked together, each a `Field`. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/Field/Label.html.twig
<label data-slot="field-label"{{ attributes.defaults({class: 'isFieldLabel'}) }}>
{##- The name of the control, which `for` points at, or the control itself inside the label. -#}
{%- block content %}{% endblock -%}
</label>
templates/components/Field/Legend.html.twig
{%- props
    ## 'legend'|'label' Whether the legend reads as a title or as the size of a label.
    variant = 'legend'
-%}
<legend data-slot="field-legend" data-variant="{{ variant }}"{{ attributes.defaults({class: 'isFieldLegend' ~ (variant is same as('label') ? ' isSmall' : '')}) }}>
{##- The name of the set of fields. -#}
{%- block content %}{% endblock -%}
</legend>
templates/components/Field/Separator.html.twig
<div role="separator" data-slot="field-separator"{{ attributes.defaults({class: 'isFieldSeparator'}) }}>
{##- An optional text in the middle of the line, such as "or". -#}
{%- block content %}{% endblock -%}
</div>
templates/components/Field/Set.html.twig
<fieldset data-slot="field-set"{{ attributes.defaults({class: 'isFieldSet'}) }}>
{##- A `Field:Legend` and the fields of the set, often in a `Field:Group`. -#}
{%- block content %}{% endblock -%}
</fieldset>
templates/components/Field/Title.html.twig
<div data-slot="field-title"{{ attributes.defaults({class: 'isFieldTitle'}) }}>
{##- A title inside a field, for a control that has its own label. -#}
{%- block content %}{% endblock -%}
</div>