Input Group
Add addons, buttons, and helper content to inputs.
Loading...
<twig:InputGroup class="max-w-xs">
<twig:InputGroup:Input placeholder="Search..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:search" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">12 results</twig:InputGroup:Addon>
</twig:InputGroup>
Installation
Note
Available since UX Toolkit 3.0.
bin/console ux:install input-group --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:
templates/components/InputGroup.html.twig
{# @block content The input group elements, typically includes input, `InputGroup:Addon`, and/or `InputGroup:Button` #}
<div
data-slot="input-group"
role="group"
class="{{ ('group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 ltr:has-[>[data-align=inline-end]]:[&>input]:pr-1.5 rtl:has-[>[data-align=inline-end]]:[&>input]:pe-1.5 ltr:has-[>[data-align=inline-start]]:[&>input]:pl-1.5 rtl:has-[>[data-align=inline-start]]:[&>input]:ps-1.5 ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</div>
templates/components/InputGroup/Addon.html.twig
{# @prop align 'inline-start'|'inline-end'|'block-start'|'block-end' The addon position relative to the input. Defaults to `inline-start` #}
{# @block content The addon content, typically an icon or text #}
{%- props align = 'inline-start' -%}
{%- set style = html_cva(
base: "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
variants: {
align: {
'inline-start': 'order-first ltr:pl-2 rtl:ps-2 ltr:has-[>button]:ml-[-0.3rem] rtl:has-[>button]:ms-[-0.3rem] ltr:has-[>kbd]:ml-[-0.15rem] rtl:has-[>kbd]:ms-[-0.15rem]',
'inline-end': 'order-last ltr:pr-2 rtl:pe-2 ltr:has-[>button]:mr-[-0.3rem] rtl:has-[>button]:me-[-0.3rem] ltr:has-[>kbd]:mr-[-0.15rem] rtl:has-[>kbd]:me-[-0.15rem]',
'block-start': 'order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2',
'block-end': 'order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2',
},
},
) -%}
<div
data-slot="input-group-addon"
data-align="{{ align }}"
role="group"
class="{{ style.apply({align: align}, attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</div>
templates/components/InputGroup/Button.html.twig
{# @prop type 'button'|'submit' The button type. Defaults to `button` #}
{# @prop variant 'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' The visual style variant. Defaults to `ghost` #}
{# @prop size 'default'|'sm'|'lg'|'icon'|'icon-sm'|'icon-lg' The button size. Defaults to `xs` #}
{# @block content The button label and/or icon #}
{%- props type = 'button', variant = 'ghost', size = 'xs' -%}
{%- set style = html_cva(
base: 'flex items-center gap-2 text-sm shadow-none',
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: '',
'icon-xs': 'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
},
},
) %}
<twig:Button
type="{{ type }}"
variant="{{ variant }}"
size="{{ size }}"
class="{{ style.apply({size: size}, attributes.render('class'))|tailwind_merge }}"
{{ ...attributes }}
>
{{- block(outerBlocks.content) -}}
</twig:Button>
templates/components/InputGroup/Input.html.twig
<twig:Input
data-slot="input-group-control"
class="{{ ('flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent ' ~ attributes.render('class'))|tailwind_merge }}"
{{ ...attributes.without('class') }}
/>
templates/components/InputGroup/Text.html.twig
{# @block content The text content displayed in the input group #}
<span
data-slot="input-group-text"
class="{{ ('text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4 ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</span>
templates/components/InputGroup/Textarea.html.twig
{# @block content The initial textarea value #}
<twig:Textarea
data-slot="input-group-control"
class="{{ ('flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent ' ~ attributes.render('class'))|tailwind_merge }}"
{{ ...attributes.without('class') }}
>{{- block(outerBlocks.content) -}}</twig: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/Input.html.twig
<input
class="{{ ('h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none 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.defaults({'data-slot': 'input'}) }}
>
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/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:InputGroup>
<twig:InputGroup:Input placeholder="Search..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:search" />
</twig:InputGroup:Addon>
</twig:InputGroup>
Examples
Icon
Loading...
<div class="grid w-full max-w-sm gap-6">
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Search..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:search" />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input type="email" placeholder="Enter your email" />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:mail" />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Card number" />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:credit-card" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">
<twig:ux:icon name="lucide:check" />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Card number" />
<twig:InputGroup:Addon align="inline-end">
<twig:ux:icon name="lucide:star" />
<twig:ux:icon name="lucide:info" />
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Text
Loading...
<div class="grid w-full max-w-sm gap-6">
<twig:InputGroup>
<twig:InputGroup:Addon>
<twig:InputGroup:Text>$</twig:InputGroup:Text>
</twig:InputGroup:Addon>
<twig:InputGroup:Input placeholder="0.00" />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>USD</twig:InputGroup:Text>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Addon>
<twig:InputGroup:Text>https://</twig:InputGroup:Text>
</twig:InputGroup:Addon>
<twig:InputGroup:Input placeholder="example.com" class="pl-0.5!" />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>.com</twig:InputGroup:Text>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Enter your username" />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>@company.com</twig:InputGroup:Text>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Textarea placeholder="Enter your message" />
<twig:InputGroup:Addon align="block-end">
<twig:InputGroup:Text class="text-xs text-muted-foreground">120 characters left</twig:InputGroup:Text>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Button
Loading...
<div class="grid w-full max-w-sm gap-6">
<twig:InputGroup>
<twig:InputGroup:Input placeholder="https://x.com/symfony" readonly />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Button aria-label="Copy" title="Copy" size="icon-xs">
<twig:ux:icon name="lucide:copy" />
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup class="[--radius:9999px]">
<twig:InputGroup:Addon>
<twig:InputGroup:Button variant="secondary" size="icon-xs">
<twig:ux:icon name="lucide:info" />
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
<twig:InputGroup:Addon class="pl-1.5 text-muted-foreground">
https://
</twig:InputGroup:Addon>
<twig:InputGroup:Input />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Button size="icon-xs">
<twig:ux:icon name="lucide:star" />
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Type to search..." />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Button variant="secondary">Search</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Kbd
Loading...
<twig:InputGroup class="max-w-sm">
<twig:InputGroup:Input placeholder="Search..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:search" class="text-muted-foreground" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">
<twig:Kbd>⌘K</twig:Kbd>
</twig:InputGroup:Addon>
</twig:InputGroup>
Spinner
Loading...
<div class="grid w-full max-w-sm gap-4">
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Searching..." />
<twig:InputGroup:Addon align="inline-end">
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Processing..." />
<twig:InputGroup:Addon>
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Saving changes..." />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>Saving...</twig:InputGroup:Text>
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Refreshing data..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:loader" class="animate-spin" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text class="text-muted-foreground">Please wait...</twig:InputGroup:Text>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Textarea
Loading...
<div class="grid w-full max-w-md gap-4">
<twig:InputGroup>
<twig:InputGroup:Textarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
class="min-h-[200px]"
/>
<twig:InputGroup:Addon align="block-end" class="border-t">
<twig:InputGroup:Text>Line 1, Column 1</twig:InputGroup:Text>
<twig:InputGroup:Button size="sm" class="ml-auto" variant="default">
Run <twig:ux:icon name="lucide:corner-down-left" />
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="block-start" class="border-b">
<twig:InputGroup:Text class="font-mono font-medium">
<twig:ux:icon name="ri:javascript-fill" />
script.js
</twig:InputGroup:Text>
<twig:InputGroup:Button class="ml-auto" size="icon-xs">
<twig:ux:icon name="lucide:refresh-ccw" />
</twig:InputGroup:Button>
<twig:InputGroup:Button variant="ghost" size="icon-xs">
<twig:ux:icon name="lucide:copy" />
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
RTL
To enable RTL support, set the dir="rtl" attribute on the root element.
Loading...
<div class="w-full flex flex-col items-center gap-16">
<div dir="rtl" class="grid w-full max-w-sm gap-6">
<twig:InputGroup class="max-w-xs">
<twig:InputGroup:Input placeholder="بحث..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:search" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">١٢ نتيجة</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="جاري البحث..." />
<twig:InputGroup:Addon align="inline-end">
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="جاري حفظ التغييرات..." />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>جاري الحفظ...</twig:InputGroup:Text>
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:Field:Group class="max-w-sm">
<twig:Field>
<twig:Field:Label for="rtl-ar-textarea">منطقة النص</twig:Field:Label>
<twig:InputGroup>
<twig:InputGroup:Textarea id="rtl-ar-textarea" placeholder="اكتب تعليقًا..." />
<twig:InputGroup:Addon align="block-end">
<twig:InputGroup:Text>٠/٢٨٠</twig:InputGroup:Text>
<twig:InputGroup:Button variant="default" size="sm" class="ms-auto">نشر</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:Field:Description>تذييل موضع أسفل منطقة النص.</twig:Field:Description>
</twig:Field>
</twig:Field:Group>
</div>
<div dir="rtl" class="grid w-full max-w-sm gap-6">
<twig:InputGroup class="max-w-xs">
<twig:InputGroup:Input placeholder="הקלד..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:search" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">12 תוצאות</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="טוען..." />
<twig:InputGroup:Addon align="inline-end">
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="שומר שינויים..." />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>שומר...</twig:InputGroup:Text>
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:Field:Group class="max-w-sm">
<twig:Field>
<twig:Field:Label for="rtl-he-textarea">אזור טקסט</twig:Field:Label>
<twig:InputGroup>
<twig:InputGroup:Textarea id="rtl-he-textarea" placeholder="כתוב תגובה..." />
<twig:InputGroup:Addon align="block-end">
<twig:InputGroup:Text>0/280</twig:InputGroup:Text>
<twig:InputGroup:Button variant="default" size="sm" class="ms-auto">שלח</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:Field:Description>כותרת תחתונה ממוקמת מתחת לאזור הטקסט.</twig:Field:Description>
</twig:Field>
</twig:Field:Group>
</div>
</div>
API Reference
Component InputGroup
| Block | Description |
|---|---|
content |
The input group elements, typically includes input, InputGroup:Addon, and/or InputGroup:Button |
Component InputGroup:Addon
| Prop | Type | Description |
|---|---|---|
align |
'inline-start'|'inline-end'|'block-start'|'block-end' |
The addon position relative to the input. Defaults to inline-start |
| Block | Description |
|---|---|
content |
The addon content, typically an icon or text |
Component InputGroup:Button
| Prop | Type | Description |
|---|---|---|
type |
'button'|'submit' |
The button type. Defaults to button |
variant |
'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' |
The visual style variant. Defaults to ghost |
size |
'default'|'sm'|'lg'|'icon'|'icon-sm'|'icon-lg' |
The button size. Defaults to xs |
| Block | Description |
|---|---|
content |
The button label and/or icon |
Component InputGroup:Text
| Block | Description |
|---|---|
content |
The text content displayed in the input group |
Component InputGroup:Textarea
| Block | Description |
|---|---|
content |
The initial textarea value |