Input Group
Wraps inputs with optional leading or trailing elements like icons or keyboard hints.
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" class="size-4" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">12 results</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="example.com" class="!pl-1" />
<twig:InputGroup:Addon>
<twig:InputGroup:Text>https://</twig:InputGroup:Text>
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">
<twig:ux:icon name="tabler:info-circle" class="size-4" title="This is content in (not) a tooltip. (yet!)"/>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Textarea placeholder="Ask, Search or Chat..." />
<twig:InputGroup:Addon align="block-end">
<twig:InputGroup:Button
variant="outline"
class="rounded-full"
size="icon-xs"
>
<twig:ux:icon name="lucide:plus" />
</twig:InputGroup:Button>
<twig:InputGroup:Text class="ml-auto">52% used</twig:InputGroup:Text>
<twig:Separator orientation="vertical" class="!h-4" />
<twig:InputGroup:Button
variant="default"
class="rounded-full"
size="icon-xs"
disabled
>
<twig:ux:icon name="tabler:circle-arrow-up-filled" class="size-5" />
<span class="sr-only">Send</span>
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="@shadcn" />
<twig:InputGroup:Addon align="inline-end">
<div class="bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full">
<twig:ux:icon name="tabler:check" />
</div>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Installation
bin/console ux:install input-group --kit shadcn
That's it!
Install the following Composer dependencies:
composer require symfony/ux-icons tales-from-a-dev/twig-tailwind-extra:^1.0.0
Copy the following file(s) into your Symfony app:
{# @block content The default block #}
<div
data-slot="input-group"
class="{{ [
'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
'h-9 min-w-0 has-[>textarea]:h-auto',
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
attributes.render('class'),
]|join(' ')|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</div>
{# @prop align 'inline-start'|'inline-end'|'block-start'|'block-end' The addon alignment, default to `inline-start` #}
{# @block content The default block #}
{%- props align = 'inline-start' -%}
{%- set style = html_cva(
base: "text-muted-foreground flex h-auto items-center justify-center gap-2 text-sm font-medium select-none [&_svg:not([class*='size-'])]:size-4 group-data-[disabled=true]/input-group:opacity-50",
variants: {
align: {
'inline-start': 'order-first pl-3',
'inline-end': 'order-last pr-3',
'block-start': 'order-first w-full justify-start px-3 pt-3 [&.border-b]:pb-3',
'block-end': 'order-last w-full justify-start px-3 pb-3 [&.border-t]:pt-3',
},
},
) -%}
<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>
{# @prop type 'button'|'submit' The type, default to `button` #}
{# @prop variant 'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' The variant, default to `default` #}
{# @prop size 'default'|'sm'|'lg'|'icon'|'icon-sm'|'icon-lg' The size, default to `default` #}
{# @block content The default block #}
{%- props type = 'button', variant = 'ghost', size = 'xs' -%}
{%- set style = html_cva(
base: 'text-sm shadow-none flex gap-2 items-center',
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: 'h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5',
'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] 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>
<input
data-slot="input-group-control"
class="{{ ('flex-1 h-full w-full rounded-none border-0 bg-transparent px-3 py-2 text-sm shadow-none file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{# @block content The default block #}
<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>
{# @block content The default block #}
<textarea
data-slot="input-group-control"
class="{{ ('flex-1 min-h-[80px] w-full resize-none rounded-none border-0 bg-transparent px-3 py-3 text-base shadow-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</textarea>
{%- props label = 'Loading' -%}
{{ ux_icon('lucide:loader-2', {
'data-slot': 'spinner',
'aria-label': label,
role: 'status',
class: 'size-4 animate-spin ' ~ attributes.render('class')|tailwind_merge,
...attributes.without('class'),
}) }}
Happy coding!
Usage
<twig:InputGroup>
<twig:InputGroup:Input placeholder="Search..." />
<twig:InputGroup:Addon>
<twig:ux:icon name="lucide:search" />
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Button>Search</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
Examples
Icon
Loading...
<div class="grid w-full max-w-sm gap-6" xmlns:twig="http://www.w3.org/1999/html">
<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 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
Display additional text information alongside inputs.
Loading...
<div class="grid w-full max-w-sm gap-6" xmlns:twig="http://www.w3.org/1999/html">
<twig:InputGroup>
<twig:InputGroup:Input placeholder="0.00" />
<twig:InputGroup:Addon>
<twig:InputGroup:Text>$</twig:InputGroup:Text>
</twig:InputGroup:Addon>
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>USD</twig:InputGroup:Text>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input placeholder="example.com" />
<twig:InputGroup:Addon>
<twig:InputGroup:Text>https://</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-muted-foreground text-xs">120 characters left</twig:InputGroup:Text>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Button
Loading...
<div class="grid w-full max-w-sm gap-6" xmlns:twig="http://www.w3.org/1999/html">
<twig:InputGroup>
<twig:InputGroup:Input placeholder="https://twitter.com/symfony" readonly />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Button title="Copy to clipboard">
<twig:ux:Icon name="lucide:copy" />
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup class="[--radius:9999px]">
<twig:InputGroup:Input placeholder="https://" />
<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 align="inline-end">
<twig:InputGroup:Button title="Copy to clipboard">
<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>
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>
<InputGroup:Button variant="ghost" size="icon-xs">
<twig:ux:icon name="lucide:copy" />
</InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Spinner
Loading...
<div class="max-w-md flex flex-col gap-4">
<twig:InputGroup data-disabled="true">
<twig:InputGroup:Input placeholder="Searching..." disabled />
<twig:InputGroup:Addon align="inline-end">
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup data-disabled="true">
<twig:InputGroup:Input placeholder="Processing..." disabled />
<twig:InputGroup:Addon>
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup data-disabled="true">
<twig:InputGroup:Input placeholder="Saving changes..." disabled />
<twig:InputGroup:Addon align="inline-end">
<twig:InputGroup:Text>Saving...</twig:InputGroup:Text>
<twig:Spinner />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup data-disabled="true">
<twig:InputGroup:Input placeholder="Refreshing data..." disabled />
<twig:InputGroup:Addon>
{{ ux_icon('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>
Label
Loading...
<div class="grid w-full max-w-sm gap-6" xmlns:twig="http://www.w3.org/1999/html">
<twig:InputGroup>
<twig:InputGroup:Input id="username" placeholder="symfony" />
<twig:InputGroup:Addon>
<twig:Label for="username">@</twig:Label>
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:InputGroup>
<twig:InputGroup:Input id="email" placeholder="symfony" />
<twig:InputGroup:Addon align="block-start">
<twig:Label for="email" class="text-foreground">Email</twig:Label>
<twig:InputGroup:Button variant="ghost" aria-label="Help" class="ml-auto rounded-full" size="xs">
<twig:ux:Icon name="lucide:help-circle" />
</twig:InputGroup:Button>
</twig:InputGroup:Addon>
</twig:InputGroup>
</div>
Button Group
Loading...
<div class="grid w-full max-w-sm gap-6" xmlns:twig="http://www.w3.org/1999/html">
<twig:ButtonGroup>
<twig:ButtonGroup:Text>
<Label for="url">https://</Label>
</twig:ButtonGroup:Text>
<twig:InputGroup>
<twig:InputGroup:Input id="url" placeholder="example.com" />
<twig:InputGroup:Addon align="inline-end">
<twig:ux:icon name="lucide:link-2" />
</twig:InputGroup:Addon>
</twig:InputGroup>
<twig:ButtonGroup:Text>
.com
</twig:ButtonGroup:Text>
</twig:ButtonGroup>
</div>
API Reference
InputGroup
| Block | Description |
|---|---|
content |
The default block |
InputGroup:Addon
| Prop | Type | Description |
|---|---|---|
align |
'inline-start'|'inline-end'|'block-start'|'block-end' |
The addon alignment, default to inline-start |
| Block | Description |
|---|---|
content |
The default block |
InputGroup:Button
| Prop | Type | Description |
|---|---|---|
type |
'button'|'submit' |
The type, default to button |
variant |
'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' |
The variant, default to default |
size |
'default'|'sm'|'lg'|'icon'|'icon-sm'|'icon-lg' |
The size, default to default |
| Block | Description |
|---|---|
content |
The default block |
InputGroup:Text
| Block | Description |
|---|---|
content |
The default block |
InputGroup:Textarea
| Block | Description |
|---|---|
content |
The default block |