Button Group

A layout helper for grouping buttons and related controls with shared borders and separators.

Loading...
<twig:ButtonGroup>
    <twig:ButtonGroup class="hidden sm:flex">
        <twig:Button variant="outline" size="icon" aria-label="Go back">
            <twig:ux:icon name="lucide:arrow-left" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline">Archive</twig:Button>
        <twig:Button variant="outline">Report</twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline">
            <twig:ux:icon name="lucide:clock" class="size-4" />
            Snooze
        </twig:Button>
        <twig:Button variant="outline" size="icon" aria-label="More options">
            <twig:ux:icon name="lucide:more-horizontal" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
</twig:ButtonGroup>

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

{# @prop orientation 'horizontal'|'vertical' The orientation, default to `horizontal` #}
{# @block content The default block #}
{%- props orientation = 'horizontal' -%}
{%- set style = html_cva(
    base: 'flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*=\'w-\'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2',
    variants: {
        orientation: {
            horizontal: '[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
            vertical: 'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
        },
    },
    default_variant: {
        orientation: 'horizontal',
    },
) -%}

<div
    role="group"
    data-slot="button-group"
    data-orientation="{{ orientation }}"
    class="{{ style.apply({orientation: orientation}, attributes.render('class'))|tailwind_merge }}"
    {{ attributes }}
>
    {%- block content %}{% endblock -%}
</div>
{# @prop orientation 'horizontal'|'vertical' The orientation of the separator, default to `vertical` #}
{# @block content The default block #}
{%- props orientation = 'vertical' -%}
<twig:Separator
    orientation="{{ orientation }}"
    data-slot="button-group-separator"
    class="bg-input relative m-0! self-stretch {{ orientation == 'vertical' ? 'h-auto' }} {{ attributes.render('class')|tailwind_merge }}"
    {{ ...attributes }}
/>
{# @prop as 'div' The HTML tag to use, default to `div` #}
{# @block content The default block #}
{%- props as = 'div' -%}
<{{ as }}
    data-slot="button-group-text"
    class="{{ 'bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 ' ~ attributes.render('class')|tailwind_merge }}"
    {{ attributes }}
>
    {%- block content %}{% endblock -%}
</{{ as }}>
{# @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>

Happy coding!

Usage

<twig:ButtonGroup>
    <twig:ButtonGroup class="hidden sm:flex">
        <twig:Button variant="outline" size="icon" aria-label="Go back">
            <twig:ux:icon name="lucide:arrow-left" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline">Archive</twig:Button>
        <twig:Button variant="outline">Report</twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline">
            <twig:ux:icon name="lucide:clock" class="size-4" />
            Snooze
        </twig:Button>
        <twig:Button variant="outline" size="icon" aria-label="More options">
            <twig:ux:icon name="lucide:more-horizontal" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
</twig:ButtonGroup>

Examples

Default

Loading...
<twig:ButtonGroup>
    <twig:ButtonGroup class="hidden sm:flex">
        <twig:Button variant="outline" size="icon" aria-label="Go back">
            <twig:ux:icon name="lucide:arrow-left" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline">Archive</twig:Button>
        <twig:Button variant="outline">Report</twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline">
            <twig:ux:icon name="lucide:clock" class="size-4" />
            Snooze
        </twig:Button>
        <twig:Button variant="outline" size="icon" aria-label="More options">
            <twig:ux:icon name="lucide:more-horizontal" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
</twig:ButtonGroup>

Orientation

Loading...
<twig:ButtonGroup orientation="vertical" aria-label="Media controls" class="h-fit">
    <twig:Button variant="outline" size="icon">
        <twig:ux:icon name="lucide:plus" class="size-4" />
    </twig:Button>
    <twig:Button variant="outline" size="icon">
        <twig:ux:icon name="lucide:minus" class="size-4" />
    </twig:Button>
</twig:ButtonGroup>

Size

Loading...
<div class="flex flex-col items-start gap-8">
    <twig:ButtonGroup>
        <twig:Button variant="outline" size="sm">Small</twig:Button>
        <twig:Button variant="outline" size="sm">Button</twig:Button>
        <twig:Button variant="outline" size="sm">Group</twig:Button>
        <twig:Button variant="outline" size="icon-sm">
            <twig:ux:icon name="lucide:plus" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline">Default</twig:Button>
        <twig:Button variant="outline">Button</twig:Button>
        <twig:Button variant="outline">Group</twig:Button>
        <twig:Button variant="outline" size="icon">
            <twig:ux:icon name="lucide:plus" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline" size="lg">Large</twig:Button>
        <twig:Button variant="outline" size="lg">Button</twig:Button>
        <twig:Button variant="outline" size="lg">Group</twig:Button>
        <twig:Button variant="outline" size="icon-lg">
            <twig:ux:icon name="lucide:plus" class="size-5" />
        </twig:Button>
    </twig:ButtonGroup>
</div>

Input

Loading...
<twig:ButtonGroup class="max-w-md">
    <twig:Input placeholder="Search..." />
    <twig:Button size="icon-lg" variant="outline" aria-label="Search">
        <twig:ux:icon name="lucide:search" class="size-4" />
    </twig:Button>
</twig:ButtonGroup>

Nested

Loading...
<twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline" size="sm">1</twig:Button>
        <twig:Button variant="outline" size="sm">2</twig:Button>
        <twig:Button variant="outline" size="sm">3</twig:Button>
        <twig:Button variant="outline" size="sm">4</twig:Button>
        <twig:Button variant="outline" size="sm">5</twig:Button>
    </twig:ButtonGroup>
    <twig:ButtonGroup>
        <twig:Button variant="outline" size="icon-sm" aria-label="Previous">
            <twig:ux:icon name="lucide:arrow-left" class="size-4" />
        </twig:Button>
        <twig:Button variant="outline" size="icon-sm" aria-label="Next">
            <twig:ux:icon name="lucide:arrow-right" class="size-4" />
        </twig:Button>
    </twig:ButtonGroup>
</twig:ButtonGroup>

Separator

Loading...
<twig:ButtonGroup>
    <twig:Button variant="secondary" size="sm">Copy</twig:Button>
    <twig:ButtonGroup:Separator />
    <twig:Button variant="secondary" size="sm">Paste</twig:Button>
</twig:ButtonGroup>

Split

Loading...
<twig:ButtonGroup>
    <twig:Button variant="secondary">Button</twig:Button>
    <twig:ButtonGroup:Separator />
    <twig:Button size="icon" variant="secondary">
        <twig:ux:icon name="tabler:plus" class="size-4" />
    </twig:Button>
</twig:ButtonGroup>

API Reference

ButtonGroup

Prop Type Description
orientation 'horizontal'|'vertical' The orientation, default to horizontal
Block Description
content The default block

ButtonGroup:Separator

Prop Type Description
orientation 'horizontal'|'vertical' The orientation of the separator, default to vertical
Block Description
content The default block

ButtonGroup:Text

Prop Type Description
as 'div' The HTML tag to use, default to div
Block Description
content The default block