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
Capacidades 8
Conteúdo
Modo de seleção (controle segmentado)
Faz o grupo agir como controle…
Separadores automáticos
Insere automaticamente o ButtonGroup.Separator da base…
Estado ocupado / desabilitar o grupo
Desabilita e marca aria-busy em todos…
Largura igual (segmentos justificados) ⚠️
Estica os botões para larguras iguais…
Comportamento
Navegação por teclado (setas)
O grupo vira um único tab-stop:…
Overflow (colapsar ou rolar)
Quando o grupo excede a largura…
Rótulo/addon de texto
Acopla um ButtonGroup.Text (ex.: 'Ordenar:', 'R$',…
Envio em formulário (name/valor)
No modo segmentado, publica o segmento…
Claude Code
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
LLM / MCP
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"
}
}
}