Pular para o conteúdo

Button Group

templates/components/ButtonGroup

Vários botões fundidos num único controle: a base colapsa bordas e achata os cantos internos (cantos externos arredondados, w-fit, items-stretch) para que ações vizinhas leiam como UMA peça. Traz subcomponentes Separator (divisor) e Text (addon tipo 'https://', 'R$', rótulo) e suporta grupos aninhados, inputs e selects encaixados. O que os enriquecedores exploram é justamente esse 'conjunto que age como um só'.

Base congelada

Conteúdo

Modo de seleção (controle segmentado)

Faz o grupo agir como controle…

Faz o grupo agir como controle segmentado: botões passam a manter estado ativo via aria-pressed/aria-checked. 'Único' vira radiogroup (um ligado por vez), 'múltiplo' permite vários ligados. Reaproveita o estado ativo já existente do Button.

Separadores automáticos

Insere automaticamente o ButtonGroup.Separator da base…

Insere automaticamente o ButtonGroup.Separator da base entre cada item, dispensando escrevê-lo à mão entre os botões. Só liga/desliga conteúdo aditivo já previsto na base.

Estado ocupado / desabilitar o grupo

Desabilita e marca aria-busy em todos…

Desabilita e marca aria-busy em todos os botões de uma vez (ex.: enquanto uma ação do grupo está em andamento), propagando o estado ao conjunto sem mexer botão a botão.

Largura igual (segmentos justificados) ⚠️

Estica os botões para larguras iguais…

Estica os botões para larguras iguais preenchendo o container (segmentos equilibrados) em vez do w-fit padrão. Incluído por honestidade: altera a largura/layout definidos pela base (w-fit), portanto é DECISÃO DE BASE, não enricher legítimo.

Comportamento

Navegação por teclado (setas)

O grupo vira um único tab-stop:…

O grupo vira um único tab-stop: as setas movem o foco entre os botões e Home/End saltam para as pontas (roving tabindex, padrão toolbar/radiogroup). Comportamento puro de a11y, sem tocar no visual.

Overflow (colapsar ou rolar)

Quando o grupo excede a largura…

Quando o grupo excede a largura disponível, esconde os botões extras num gatilho '…' (menu 'mais') ou torna a fila rolável na horizontal — sem quebrar o encaixe visual dos botões restantes. posicionável

Rótulo/addon de texto

Acopla um ButtonGroup.Text (ex.: 'Ordenar:', 'R$',…

Acopla um ButtonGroup.Text (ex.: 'Ordenar:', 'R$', 'https://') encaixado no início ou no fim do grupo, como addon de formulário. Usa o subcomponente Text da base; você só escolhe o texto e o lado. posicionável

Envio em formulário (name/valor)

No modo segmentado, publica o segmento…

No modo segmentado, publica o segmento ativo como valor de formulário via input hidden com 'name', fazendo o grupo submeter como um radio/checkbox nativo. Puro comportamento de dados.
Código gerado

Cole no Claude Code — ele acerta de primeira.

ButtonGroup (UI) — Design System (Symfony UX Toolkit / shadcn)

BASE CONGELADA (não mude por instância): A fusão visual (colapso de bordas, cantos externos arredondados + internos achatados, w-fit, items-stretch, gap-2 dos grupos aninhados), a prop de estrutura orientation (horizontal/vertical), o tratamento LTR/RTL, os tokens/estilo dos subcomponentes Separator e Text, além da fonte/tamanho/cor/padding herdados de cada Button. A11y de base: role='group', data-slot, data-orientation. Nada disso muda por instância.

USO: <twig:ButtonGroup />

CAPACIDADES OPT-IN (ligue sem alterar o visual):
- Modo de seleção (controle segmentado): Faz o grupo agir como controle segmentado: botões passam a manter estado ativo via aria-pressed/aria-checked. 'Único' vira radiogroup (um ligado por vez), 'múltiplo' permite vários ligados. Reaproveita o estado ativo já existente do Button. [select → nenhum / único (radio) / múltiplo (toggle)]- Navegação por teclado (setas): O grupo vira um único tab-stop: as setas movem o foco entre os botões e Home/End saltam para as pontas (roving tabindex, padrão toolbar/radiogroup). Comportamento puro de a11y, sem tocar no visual. [toggle]- Overflow (colapsar ou rolar): Quando o grupo excede a largura disponível, esconde os botões extras num gatilho '…' (menu 'mais') ou torna a fila rolável na horizontal — sem quebrar o encaixe visual dos botões restantes. [select → nenhum / menu '…' (mais) / rolar horizontal]- Separadores automáticos: Insere automaticamente o ButtonGroup.Separator da base entre cada item, dispensando escrevê-lo à mão entre os botões. Só liga/desliga conteúdo aditivo já previsto na base. [toggle]- Rótulo/addon de texto: Acopla um ButtonGroup.Text (ex.: 'Ordenar:', 'R$', 'https://') encaixado no início ou no fim do grupo, como addon de formulário. Usa o subcomponente Text da base; você só escolhe o texto e o lado. [text]- Estado ocupado / desabilitar o grupo: Desabilita e marca aria-busy em todos os botões de uma vez (ex.: enquanto uma ação do grupo está em andamento), propagando o estado ao conjunto sem mexer botão a botão. [toggle]- Envio em formulário (name/valor): No modo segmentado, publica o segmento ativo como valor de formulário via input hidden com 'name', fazendo o grupo submeter como um radio/checkbox nativo. Puro comportamento de dados. [text]- Largura igual (segmentos justificados): Estica os botões para larguras iguais preenchendo o container (segmentos equilibrados) em vez do w-fit padrão. Incluído por honestidade: altera a largura/layout definidos pela base (w-fit), portanto é DECISÃO DE BASE, não enricher legítimo. [toggle]  (⚠️ borderline: revisar se não é decisão de base)FAÇA:
- Use <twig:Nome> — a base é congelada, você é dono do template em templates/components/.
- Ligue apenas capacidades opt-in listadas (e_funcao=true); a aparência não muda ao ligá-las.
- Passe atributos extras (id, aria-*, name, data-*) via {{ attributes }}.
NÃO FAÇA:
- Não mude cor/fonte/cantos/espaçamento por instância — é decisão de BASE, na fonte única assets/styles/app.css.
- Não crie variante/fork para a mesma coisa — existe UMA base por componente.
- Não reimplemente o componente nem adicione toolchain Node.
TESTE ANTES DE MUDAR: "é função ou é base?" — função = capacidade opt-in; base = mude o token na fonte única (assets/styles/app.css), para todos.

A11Y (herdada da base): A11y de base: role='group', data-slot, data-orientation

Via MCP: tool get_component com {"id": "button-group"} · list_capabilities("button-group").

Spec crua: config/ds-specs/button-group.json · Conectar o MCP.

Spec machine-readable (JSON)

{
    "$schema_version": "1.0",
    "id": "button-group",
    "component": "ButtonGroup",
    "eixo": "ui",
    "particularidade": "Vários botões fundidos num único controle: a base colapsa bordas e achata os cantos internos (cantos externos arredondados, w-fit, items-stretch) para que ações vizinhas leiam como UMA peça. Traz subcomponentes Separator (divisor) e Text (addon tipo 'https://', 'R$', rótulo) e suporta grupos aninhados, inputs e selects encaixados. O que os enriquecedores exploram é justamente esse 'conjunto que age como um só'.",
    "base_congelada": "A fusão visual (colapso de bordas, cantos externos arredondados + internos achatados, w-fit, items-stretch, gap-2 dos grupos aninhados), a prop de estrutura orientation (horizontal/vertical), o tratamento LTR/RTL, os tokens/estilo dos subcomponentes Separator e Text, além da fonte/tamanho/cor/padding herdados de cada Button. A11y de base: role='group', data-slot, data-orientation. Nada disso muda por instância.",
    "props": [
        {
            "name": "orientation",
            "type": "'horizontal'|'vertical'",
            "default": "horizontal",
            "description": "The layout direction of the button group. Defaults to `horizontal` #}"
        }
    ],
    "capacidades": [
        {
            "id": "modo-de-sele-o-controle-segmentado",
            "nome": "Modo de seleção (controle segmentado)",
            "descricao": "Faz o grupo agir como controle segmentado: botões passam a manter estado ativo via aria-pressed/aria-checked. 'Único' vira radiogroup (um ligado por vez), 'múltiplo' permite vários ligados. Reaproveita o estado ativo já existente do Button.",
            "controle": "select",
            "opcoes": [
                "nenhum",
                "único (radio)",
                "múltiplo (toggle)"
            ],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/button-group; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "navega-o-por-teclado-setas",
            "nome": "Navegação por teclado (setas)",
            "descricao": "O grupo vira um único tab-stop: as setas movem o foco entre os botões e Home/End saltam para as pontas (roving tabindex, padrão toolbar/radiogroup). Comportamento puro de a11y, sem tocar no visual.",
            "controle": "toggle",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/button-group; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "overflow-colapsar-ou-rolar",
            "nome": "Overflow (colapsar ou rolar)",
            "descricao": "Quando o grupo excede a largura disponível, esconde os botões extras num gatilho '…' (menu 'mais') ou torna a fila rolável na horizontal — sem quebrar o encaixe visual dos botões restantes.",
            "controle": "select",
            "opcoes": [
                "nenhum",
                "menu '…' (mais)",
                "rolar horizontal"
            ],
            "posicionavel": true,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/button-group; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "separadores-autom-ticos",
            "nome": "Separadores automáticos",
            "descricao": "Insere automaticamente o ButtonGroup.Separator da base entre cada item, dispensando escrevê-lo à mão entre os botões. Só liga/desliga conteúdo aditivo já previsto na base.",
            "controle": "toggle",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/button-group; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "r-tulo-addon-de-texto",
            "nome": "Rótulo/addon de texto",
            "descricao": "Acopla um ButtonGroup.Text (ex.: 'Ordenar:', 'R$', 'https://') encaixado no início ou no fim do grupo, como addon de formulário. Usa o subcomponente Text da base; você só escolhe o texto e o lado.",
            "controle": "text",
            "opcoes": [],
            "posicionavel": true,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (text). Ligue no configurador em /vitrine/ui/button-group; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "estado-ocupado-desabilitar-o-grupo",
            "nome": "Estado ocupado / desabilitar o grupo",
            "descricao": "Desabilita e marca aria-busy em todos os botões de uma vez (ex.: enquanto uma ação do grupo está em andamento), propagando o estado ao conjunto sem mexer botão a botão.",
            "controle": "toggle",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/button-group; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "envio-em-formul-rio-name-valor",
            "nome": "Envio em formulário (name/valor)",
            "descricao": "No modo segmentado, publica o segmento ativo como valor de formulário via input hidden com 'name', fazendo o grupo submeter como um radio/checkbox nativo. Puro comportamento de dados.",
            "controle": "text",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (text). Ligue no configurador em /vitrine/ui/button-group; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "largura-igual-segmentos-justificados",
            "nome": "Largura igual (segmentos justificados)",
            "descricao": "Estica os botões para larguras iguais preenchendo o container (segmentos equilibrados) em vez do w-fit padrão. Incluído por honestidade: altera a largura/layout definidos pela base (w-fit), portanto é DECISÃO DE BASE, não enricher legítimo.",
            "controle": "toggle",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": false,
            "como_plugar": "REPROVADO no teste 'é função ou é base' — NÃO plugar como capacidade. É decisão de BASE: mude o token na fonte única assets/styles/app.css, valendo para TODOS os componentes."
        }
    ],
    "snippet_uso": "<twig:ButtonGroup />",
    "exemplo_demo": "<twig:ButtonGroup>\n    <twig:ButtonGroup class=\"hidden sm:flex\">\n        <twig:Button variant=\"outline\" size=\"icon\" aria-label=\"Go back\">\n            <twig:ux:icon name=\"lucide:arrow-left\" />\n        </twig:Button>\n    </twig:ButtonGroup>\n    <twig:ButtonGroup>\n        <twig:Button variant=\"outline\">Archive</twig:Button>\n        <twig:Button variant=\"outline\">Report</twig:Button>\n    </twig:ButtonGroup>\n    <twig:ButtonGroup>\n        <twig:Button variant=\"outline\">\n            <twig:ux:icon name=\"lucide:clock\" />\n            Snooze\n        </twig:Button>\n        <twig:Button variant=\"outline\" size=\"icon\" aria-label=\"More options\">\n            <twig:ux:icon name=\"lucide:more-horizontal\" />\n        </twig:Button>\n    </twig:ButtonGroup>\n</twig:ButtonGroup>",
    "regras": {
        "faca": [
            "Use <twig:Nome> — a base é congelada, você é dono do template em templates/components/.",
            "Ligue apenas capacidades opt-in listadas (e_funcao=true); a aparência não muda ao ligá-las.",
            "Passe atributos extras (id, aria-*, name, data-*) via {{ attributes }}."
        ],
        "nao_faca": [
            "Não mude cor/fonte/cantos/espaçamento por instância — é decisão de BASE, na fonte única assets/styles/app.css.",
            "Não crie variante/fork para a mesma coisa — existe UMA base por componente.",
            "Não reimplemente o componente nem adicione toolchain Node."
        ]
    },
    "a11y": "A11y de base: role='group', data-slot, data-orientation",
    "mcp": {
        "tool": "get_component",
        "args": {
            "id": "button-group"
        }
    }
}