Textarea
Displays a form textarea or a component that looks like a textarea.
Loading...
<div class="*:max-w-xs contents">
<twig:Textarea placeholder="Type your message here." />
</div>
Installation
bin/console ux:install textarea --kit shadcn
That's it!
Install the following Composer dependencies:
composer require tales-from-a-dev/twig-tailwind-extra:^1.0.0 twig/extra-bundle twig/html-extra:^3.12.0
Copy the following file(s) into your Symfony app:
templates/components/Textarea.html.twig
{# @block content The initial textarea value #}
<textarea
data-slot="textarea"
class="{{ ('flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</textarea>
templates/components/Button.html.twig
{# @prop variant 'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' The visual style variant. Defaults to `default` #}
{# @prop size 'default'|'xs'|'sm'|'lg'|'icon'|'icon-xs'|'icon-sm'|'icon-lg' The button size. Defaults to `default` #}
{# @prop as 'button' The HTML tag to render. Defaults to `button` #}
{# @block content The button label and/or icon #}
{%- props variant = 'default', size = 'default', as = 'button' -%}
{%- set style = html_cva(
base: "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-8 gap-1.5 px-2.5 ltr:has-data-[icon=inline-end]:pr-2 rtl:has-data-[icon=inline-end]:pe-2 ltr:has-data-[icon=inline-start]:pl-2 rtl:has-data-[icon=inline-start]:ps-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg ltr:has-data-[icon=inline-end]:pr-1.5 rtl:has-data-[icon=inline-end]:pe-1.5 ltr:has-data-[icon=inline-start]:pl-1.5 rtl:has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg ltr:has-data-[icon=inline-end]:pr-1.5 rtl:has-data-[icon=inline-end]:pe-1.5 ltr:has-data-[icon=inline-start]:pl-1.5 rtl:has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 ltr:has-data-[icon=inline-end]:pr-2 rtl:has-data-[icon=inline-end]:pe-2 ltr:has-data-[icon=inline-start]:pl-2 rtl:has-data-[icon=inline-start]:ps-2',
icon: 'size-8',
'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
) -%}
<{{ as }}
data-slot="button"
data-variant="{{ variant }}"
data-size="{{ size }}"
class="{{ style.apply({variant: variant, size: size}, attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</{{ as }}>
templates/components/Field.html.twig
{# @prop orientation 'vertical'|'horizontal'|'responsive' The layout direction of the field. Defaults to `vertical` #}
{# @block content The field content, typically includes `Field:Label` and form input(s) #}
{%- props orientation = 'vertical' -%}
{%- set style = html_cva(
base: 'group/field flex w-full gap-2 data-[invalid=true]:text-destructive',
variants: {
orientation: {
vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
horizontal: 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto 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:has-[>[data-slot=field-content]]:items-start @md/field-group:*:w-auto @md/field-group:*:data-[slot=field-label]:flex-auto @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 input and supplementary elements like `Field:Description` and `Field:Error` #}
<div
data-slot="field-content"
class="{{ ('group/field-content flex flex-1 flex-col gap-0.5 leading-snug ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</div>
templates/components/Field/Description.html.twig
{# @block content The helper text describing the field #}
<p
data-slot="field-description"
class="{{ ('ltr:text-left rtl:text-start text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5 last:mt-0 nth-last-2:-mt-1 [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</p>
templates/components/Field/Error.html.twig
{# @prop errors array A list of error messages (strings or objects with a `message` property). Defaults to `[]` #}
{# @block content Custom error content, overrides the `errors` prop if provided #}
{%- 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="ltr:ml-4 rtl:ms-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 grouped fields, typically multiple `Field` components #}
<div
data-slot="field-group"
class="{{ ('group/field-group @container/field-group flex w-full flex-col gap-5 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 label text for the field #}
<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-checked:border-primary/30 has-data-checked:bg-primary/5 has-checked:border-primary/30 has-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 dark:has-checked:border-primary/20 dark:has-checked:bg-primary/10 has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col ' ~ attributes.render('class'))|tailwind_merge }}"
{{ ...attributes }}
>
{{- block(outerBlocks.content) -}}
</twig:Label>
templates/components/Field/Legend.html.twig
{# @prop variant 'legend'|'label' The text size variant. Defaults to `legend` #}
{# @block content The legend text for a fieldset #}
{%- props variant = 'legend' -%}
<legend
data-slot="field-legend"
data-variant="{{ variant }}"
class="{{ ('mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</legend>
templates/components/Field/Separator.html.twig
{# @block content Optional text displayed in the center of the separator #}
{%- 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="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{{ content }}
</span>
{%- endif -%}
</div>
templates/components/Field/Set.html.twig
{# @block content The fieldset content, typically includes `Field:Legend` and `Field` components #}
<fieldset
data-slot="field-set"
class="{{ ('flex flex-col gap-4 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 title text for the field (non-label variant) #}
<div
data-slot="field-label"
class="{{ ('flex w-fit items-center gap-2 text-sm 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 separator orientation. Defaults to `horizontal` #}
{# @prop decorative boolean Whether the separator is purely decorative (not semantic). Defaults to `true` #}
{%- props orientation = 'horizontal', decorative = true -%}
<div
class="{{ ('shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes.defaults({
'data-slot': 'separator',
'data-orientation': orientation,
role: decorative ? 'none' : 'separator',
'aria-orientation': decorative ? false : orientation,
}) }}
></div>
templates/components/Label.html.twig
{# @block content The label text for a form control #}
<label
data-slot="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.without('class') }}
>
{%- block content %}{% endblock -%}
</label>
Happy coding!
Usage
<twig:Textarea />
Examples
Field
Use Field, Field:Label, and Field:Description to create a textarea with a label and description.
Loading...
<div class="*:max-w-xs contents">
<twig:Field>
<twig:Field:Label for="textarea-message">Message</twig:Field:Label>
<twig:Field:Description>Enter your message below.</twig:Field:Description>
<twig:Textarea id="textarea-message" placeholder="Type your message here." />
</twig:Field>
</div>
Disabled
Use the disabled attribute to disable the textarea. To style the disabled state, add the data-disabled attribute to the Field component.
Loading...
<div class="*:max-w-xs contents">
<twig:Field data-disabled>
<twig:Field:Label for="textarea-disabled">Message</twig:Field:Label>
<twig:Textarea id="textarea-disabled" placeholder="Type your message here." disabled />
</twig:Field>
</div>
Invalid
Use the aria-invalid attribute to mark the textarea as invalid. To style the invalid state, add the data-invalid attribute to the Field component.
Loading...
<div class="*:max-w-xs contents">
<twig:Field data-invalid>
<twig:Field:Label for="textarea-invalid">Message</twig:Field:Label>
<twig:Textarea id="textarea-invalid" placeholder="Type your message here." aria-invalid="true" />
<twig:Field:Description>Please enter a valid message.</twig:Field:Description>
</twig:Field>
</div>
Button
Pair with Button to create a textarea with a submit button.
Loading...
<div class="*:max-w-xs contents">
<div class="grid w-full gap-2">
<twig:Textarea placeholder="Type your message here." />
<twig:Button>Send message</twig:Button>
</div>
</div>
RTL
To enable RTL support, set the dir="rtl" attribute on the root element.
Loading...
<div class="*:max-w-xs contents">
<div class="flex flex-col gap-8 w-full">
{# Arabic #}
<twig:Field dir="rtl">
<twig:Field:Label for="feedback-ar" dir="rtl">التعليقات</twig:Field:Label>
<twig:Textarea id="feedback-ar" placeholder="تعليقاتك تساعدنا على التحسين..." dir="rtl" rows="4" />
<twig:Field:Description dir="rtl">شاركنا أفكارك حول خدمتنا.</twig:Field:Description>
</twig:Field>
{# Hebrew #}
<twig:Field dir="rtl">
<twig:Field:Label for="feedback-he" dir="rtl">משוב</twig:Field:Label>
<twig:Textarea id="feedback-he" placeholder="המשוב שלך עוזר לנו להתקדם..." dir="rtl" rows="4" />
<twig:Field:Description dir="rtl">שתף את מחשבותיך על השירות שלנו.</twig:Field:Description>
</twig:Field>
</div>
</div>
API Reference
Component Textarea
| Block | Description |
|---|---|
content |
The initial textarea value |