Field

Layout helpers for form fields with labels, descriptions, errors, grouping, and separators.

Loading...
<div class="w-full max-w-md">
    <twig:Field:Set>
        <twig:Field:Group>
            <twig:Field>
                <twig:Field:Label for="username">Username</twig:Field:Label>
                <twig:Input id="username" type="text" placeholder="Max Leiter" />
                <twig:Field:Description>
                    Choose a unique username for your account.
                </twig:Field:Description>
            </twig:Field>
            <twig:Field>
                <twig:Field:Label for="password">Password</twig:Field:Label>
                <twig:Field:Description>
                    Must be at least 8 characters long.
                </twig:Field:Description>
                <twig:Input id="password" type="password" placeholder="********" />
            </twig:Field>
        </twig:Field:Group>
    </twig:Field:Set>
</div>

Installation

Ensure the Symfony UX Toolkit is installed in your Symfony app:

$ composer require --dev symfony/ux-toolkit

Then, run the following command to install the component and its dependencies:

$ bin/console ux:install field --kit shadcn

The UX Toolkit is not mandatory to install a component. You can install it manually by following the next steps:

  1. Copy the following file(s) into your Symfony app:
    templates/components/Field.html.twig
    {# @prop orientation 'vertical'|'horizontal'|'responsive' The orientation of the field, default to `vertical` #}
    {# @block content The default block #}
    {%- props orientation = 'vertical' -%}
    {%- set style = html_cva(
        base: 'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
        variants: {
            orientation: {
                vertical: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto',
                horizontal: 'flex-row items-center [&>[data-slot=field-label]]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
                responsive: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto @md/field-group:[&>[data-slot=field-label]]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
            },
        },
    ) -%}
    
    <div
        role="group"
        data-slot="field"
        data-orientation="{{ orientation }}"
        class="{{ style.apply({orientation: orientation}, attributes.render('class'))|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </div>
    
    templates/components/Field/Content.html.twig
    {# @block content The default block #}
    <div
        data-slot="field-content"
        class="{{ 'group/field-content flex flex-1 flex-col gap-1.5 leading-snug ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </div>
    
    templates/components/Field/Description.html.twig
    {# @block content The default block #}
    <p
        data-slot="field-description"
        class="{{ 'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5 [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </p>
    
    templates/components/Field/Error.html.twig
    {# @prop errors array A list of errors (string or objects with a `message` field), defaults to `[]` #}
    {# @block content The default block #}
    {%- props errors = [] -%}
    {%- set slot_content -%}{%- block content %}{% endblock -%}{%- endset -%}
    {%- set slot_content = slot_content|trim -%}
    
    {%- if slot_content == '' -%}
        {%- set messages = [] -%}
        {%- for error in errors|default([]) -%}
            {%- set message = error.message ?? error -%}
            {%- if message is not same as(false) and message is not null and message != '' and not (message in messages) -%}
                {%- set messages = messages|merge([message]) -%}
            {%- endif -%}
        {%- endfor -%}
    {%- endif -%}
    
    {%- if slot_content != '' or (messages ?? [])|length > 0 -%}
        <div
            role="alert"
            data-slot="field-error"
            class="{{ 'text-destructive text-sm font-normal ' ~ attributes.render('class')|tailwind_merge }}"
            {{ attributes }}
        >
            {%- if slot_content != '' -%}
                {{ slot_content }}
            {%- elseif (messages ?? [])|length == 1 -%}
                {{ messages[0] }}
            {%- else -%}
                <ul class="ml-4 flex list-disc flex-col gap-1">
                    {%- for message in messages|default([]) -%}
                        <li>{{ message }}</li>
                    {%- endfor -%}
                </ul>
            {%- endif -%}
        </div>
    {%- endif -%}
    
    templates/components/Field/Group.html.twig
    {# @block content The default block #}
    <div
        data-slot="field-group"
        class="{{ 'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4 ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </div>
    
    templates/components/Field/Label.html.twig
    {# @block content The default block #}
    <twig:Label
        data-slot="field-label"
        class="{{ 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4 has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10 ' ~ attributes.render('class')|tailwind_merge }}"
        {{ ...attributes }}
    >
        {{- block(outerBlocks.content) -}}
    </twig:Label>
    
    templates/components/Field/Legend.html.twig
    {# @prop variant 'legend'|'label' The variant, default to `legend` #}
    {# @block content The default block #}
    {%- props variant = 'legend' -%}
    {%- set style = html_cva(
        base: 'mb-3 font-medium',
        variants: {
            variant: {
                legend: 'text-base',
                label: 'text-sm',
            },
        },
    ) %}
    <legend
        data-slot="field-legend"
        data-variant="{{ variant }}"
        class="{{ style.apply({variant: variant}, attributes.render('class'))|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </legend>
    
    templates/components/Field/Separator.html.twig
    {# @block content The default block #}
    {%- set content -%}{%- block content %}{% endblock -%}{%- endset -%}
    {%- set has_content = content|trim != '' -%}
    <div
        data-slot="field-separator"
        data-content="{{ has_content ? 'true' : 'false' }}"
        class="{{ 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        <twig:Separator class="absolute inset-0 top-1/2" />
        {%- if has_content -%}
            <span
                class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
                data-slot="field-separator-content"
            >
                {{ content }}
            </span>
        {%- endif -%}
    </div>
    
    templates/components/Field/Set.html.twig
    {# @block content The default block #}
    <fieldset
        data-slot="field-set"
        class="{{ 'flex flex-col gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </fieldset>
    
    templates/components/Field/Title.html.twig
    {# @block content The default block #}
    <div
        data-slot="field-label"
        class="{{ 'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50 ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </div>
    
    templates/components/Separator.html.twig
    {# @prop orientation 'horizontal'|'vertical' The orientation of the separator, default to `horizontal` #}
    {# @prop decorative boolean Whether the separator is decorative or not, default to `true` #}
    {%- props orientation = 'horizontal', decorative = true -%}
    {%- set style = html_cva(
        base: 'shrink-0 bg-border',
        variants: {
            orientation: {
                horizontal: 'h-[1px] w-full',
                vertical: 'h-full w-[1px]',
            },
        },
    ) -%}
    <div
        class="{{ style.apply({orientation: orientation, decorative: decorative}, attributes.render('class'))|tailwind_merge }}"
        {{ attributes.defaults({
            role: decorative ? 'none' : 'separator',
            'aria-orientation': decorative ? false : orientation,
        }) }}
    ></div>
    
    templates/components/Label.html.twig
    {# @block content The default block #}
    <label
        class="{{ ('flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 ' ~ attributes.render('class'))|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </label>
    
  2. If necessary, install the following Composer dependencies:
    $ composer require twig/extra-bundle twig/html-extra:^3.12.0 tales-from-a-dev/twig-tailwind-extra:^1.0.0
  3. And the most important, enjoy!

Usage

<div class="w-full max-w-md">
    <twig:Field:Set>
        <twig:Field:Group>
            <twig:Field>
                <twig:Field:Label for="username">Username</twig:Field:Label>
                <twig:Input id="username" type="text" placeholder="Max Leiter" />
                <twig:Field:Description>
                    Choose a unique username for your account.
                </twig:Field:Description>
            </twig:Field>
            <twig:Field>
                <twig:Field:Label for="password">Password</twig:Field:Label>
                <twig:Field:Description>
                    Must be at least 8 characters long.
                </twig:Field:Description>
                <twig:Input id="password" type="password" placeholder="********" />
            </twig:Field>
        </twig:Field:Group>
    </twig:Field:Set>
</div>

Examples

Input

Loading...
<div class="w-full max-w-md">
    <twig:Field:Set>
        <twig:Field:Group>
            <twig:Field>
                <twig:Field:Label for="username">Username</twig:Field:Label>
                <twig:Input id="username" type="text" placeholder="Max Leiter" />
                <twig:Field:Description>
                    Choose a unique username for your account.
                </twig:Field:Description>
            </twig:Field>
            <twig:Field>
                <twig:Field:Label for="password">Password</twig:Field:Label>
                <twig:Field:Description>
                    Must be at least 8 characters long.
                </twig:Field:Description>
                <twig:Input id="password" type="password" placeholder="********" />
            </twig:Field>
        </twig:Field:Group>
    </twig:Field:Set>
</div>

Textarea

Loading...
<div class="w-full max-w-md">
    <twig:Field:Set>
        <twig:Field:Group>
            <twig:Field>
                <twig:Field:Label for="feedback">Feedback</twig:Field:Label>
                <twig:Textarea
                    id="feedback"
                    placeholder="Your feedback helps us improve..."
                    rows="4"
                />
                <twig:Field:Description>
                    Share your thoughts about our service.
                </twig:Field:Description>
            </twig:Field>
        </twig:Field:Group>
    </twig:Field:Set>
</div>

Select

Loading...
<div class="w-full max-w-md">
    <twig:Field>
        <twig:Field:Label for="department">Department</twig:Field:Label>
        <twig:Select id="department">
            <option value="engineering">Engineering</option>
            <option value="design">Design</option>
            <option value="marketing">Marketing</option>
            <option value="sales">Sales</option>
            <option value="support">Customer Support</option>
            <option value="hr">Human Resources</option>
            <option value="finance">Finance</option>
            <option value="operations">Operations</option>
        </twig:Select>
        <twig:Field:Description>
            Select your department or area of work.
        </twig:Field:Description>
    </twig:Field>
</div>

Field set

Loading...
<div class="w-full max-w-md space-y-6">
    <twig:Field:Set>
        <twig:Field:Legend>Address information</twig:Field:Legend>
        <twig:Field:Description>
            We need your address to deliver your order.
        </twig:Field:Description>
        <twig:Field:Group>
            <twig:Field>
                <twig:Field:Label for="street">Street address</twig:Field:Label>
                <twig:Input id="street" type="text" placeholder="123 Main St" />
            </twig:Field>
            <div class="grid grid-cols-2 gap-4">
                <twig:Field>
                    <twig:Field:Label for="city">City</twig:Field:Label>
                    <twig:Input id="city" type="text" placeholder="New York" />
                </twig:Field>
                <twig:Field>
                    <twig:Field:Label for="zip">Postal code</twig:Field:Label>
                    <twig:Input id="zip" type="text" placeholder="90502" />
                </twig:Field>
            </div>
        </twig:Field:Group>
    </twig:Field:Set>
</div>

Checkbox

Loading...
<div class="w-full max-w-md">
    <twig:Field:Group>
        <twig:Field:Set>
            <twig:Field:Legend variant="label">
                Show these items on the desktop
            </twig:Field:Legend>
            <twig:Field:Description>
                Select the items you want to show on the desktop.
            </twig:Field:Description>
            <twig:Field:Group class="gap-3">
                <twig:Field orientation="horizontal">
                    <twig:Checkbox id="finder-pref-9k2-hard-disks-ljj" />
                    <twig:Field:Label
                        for="finder-pref-9k2-hard-disks-ljj"
                        class="font-normal"
                        checked
                    >
                        Hard disks
                    </twig:Field:Label>
                </twig:Field>
                <twig:Field orientation="horizontal">
                    <twig:Checkbox id="finder-pref-9k2-external-disks-1yg" />
                    <twig:Field:Label
                        for="finder-pref-9k2-external-disks-1yg"
                        class="font-normal"
                    >
                        External disks
                    </twig:Field:Label>
                </twig:Field>
                <twig:Field orientation="horizontal">
                    <twig:Checkbox id="finder-pref-9k2-cds-dvds-fzt" />
                    <twig:Field:Label
                        for="finder-pref-9k2-cds-dvds-fzt"
                        class="font-normal"
                    >
                        CDs, DVDs, and iPods
                    </twig:Field:Label>
                </twig:Field>
                <twig:Field orientation="horizontal">
                    <twig:Checkbox id="finder-pref-9k2-connected-servers-6l2" />
                    <twig:Field:Label
                        for="finder-pref-9k2-connected-servers-6l2"
                        class="font-normal"
                    >
                        Connected servers
                    </twig:Field:Label>
                </twig:Field>
            </twig:Field:Group>
        </twig:Field:Set>
        <twig:Field:Separator />
        <twig:Field orientation="horizontal">
            <twig:Checkbox id="finder-pref-9k2-sync-folders-nep" checked />
            <twig:Field:Content>
                <twig:Field:Label for="finder-pref-9k2-sync-folders-nep">
                    Sync Desktop & Documents folders
                </twig:Field:Label>
                <twig:Field:Description>
                    Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.
                </twig:Field:Description>
            </twig:Field:Content>
        </twig:Field>
    </twig:Field:Group>
</div>

Switch

Loading...
<div class="w-full max-w-md">
    <twig:Field orientation="horizontal">
        <twig:Field:Content>
            <twig:Field:Label for="2fa">Multi-factor authentication</twig:Field:Label>
            <twig:Field:Description>
                Enable multi-factor authentication. If you do not have a two-factor device, you can use a one-time code sent to your email.
            </twig:Field:Description>
        </twig:Field:Content>
        <twig:Switch id="2fa" />
    </twig:Field>
</div>

Field group

Loading...
<div class="w-full max-w-md">
    <twig:Field:Group>
        <twig:Field:Set>
            <twig:Field:Label>Responses</twig:Field:Label>
            <twig:Field:Description>
                Get notified when ChatGPT responds to requests that take time, like research or image generation.
            </twig:Field:Description>
            <twig:Field:Group data-slot="checkbox-group">
                <twig:Field orientation="horizontal">
                    <twig:Checkbox id="push" checked disabled />
                    <twig:Field:Label for="push" class="font-normal">
                        Push notifications
                    </twig:Field:Label>
                </twig:Field>
            </twig:Field:Group>
        </twig:Field:Set>
        <twig:Field:Separator />
        <twig:Field:Set>
            <twig:Field:Label>Tasks</twig:Field:Label>
            <twig:Field:Description>
                Get notified when tasks you've created have updates. <a href="#">Manage tasks</a>
            </twig:Field:Description>
            <twig:Field:Group data-slot="checkbox-group">
                <twig:Field orientation="horizontal">
                    <twig:Checkbox id="push-tasks" />
                    <twig:Field:Label for="push-tasks" class="font-normal">
                        Push notifications
                    </twig:Field:Label>
                </twig:Field>
                <twig:Field orientation="horizontal">
                    <twig:Checkbox id="email-tasks" />
                    <twig:Field:Label for="email-tasks" class="font-normal">
                        Email notifications
                    </twig:Field:Label>
                </twig:Field>
            </twig:Field:Group>
        </twig:Field:Set>
    </twig:Field:Group>
</div>

API Reference

Field

Prop Type Description
orientation 'vertical'|'horizontal'|'responsive' The orientation of the field, default to vertical
Block Description
content The default block

Field:Content

Block Description
content The default block

Field:Description

Block Description
content The default block

Field:Error

Prop Type Description
errors array A list of errors (string or objects with a message field), defaults to []
Block Description
content The default block

Field:Group

Block Description
content The default block

Field:Label

Block Description
content The default block

Field:Legend

Prop Type Description
variant 'legend'|'label' The variant, default to legend
Block Description
content The default block

Field:Separator

Block Description
content The default block

Field:Set

Block Description
content The default block

Field:Title

Block Description
content The default block