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, install the recipe and its dependencies by running:
bin/console ux:install Field --kit shadcn
That's it!
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
Copy the following file(s) into your Symfony app:
{# @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>
{# @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>
{# @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>
{# @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 -%}
{# @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>
{# @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>
{# @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>
{# @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>
{# @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>
{# @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>
{# @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>
{# @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>
Happy coding!
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 |