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:
- 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>
- 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
- 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 |