Pular para o conteúdo

Button

templates/components/Button

O Button é o gatilho de ação: sua identidade é o clique e o ciclo de vida do que ele dispara (intenção → confirmação → em-voo → resultado → cooldown). Por isso seus enriquecedores governam esse ciclo e as affordances do gatilho, nunca a aparência.

Base congelada

Conteúdo

Ícone (posição início/fim)

Adiciona um ícone no slot que…

Adiciona um ícone no slot que a própria base já reserva (data-icon=inline-start/inline-end, com o padding correto). Escolhe-se apenas onde ele aparece. posicionável

Badge de contagem

Sobrepõe um Badge numérico (notificações, itens…

Sobrepõe um Badge numérico (notificações, itens no carrinho) posicionado sobre o botão sem reflow — composição aditiva, não altera o visual da base do Button. posicionável

Comportamento

Estado de carregamento

Enquanto a ação está em voo,…

Enquanto a ação está em voo, troca o conteúdo por um Spinner (componente existente), marca aria-busy e bloqueia o re-clique; volta sozinho ao concluir.

Confirmação antes de agir

Intercepta o clique e exige uma…

Intercepta o clique e exige uma confirmação antes de disparar a ação — útil sobretudo com a variante destructive. Puro comportamento, sem alterar o botão.

Prevenção de duplo envio

Após o clique, desabilita o botão…

Após o clique, desabilita o botão por N milissegundos (reusa disabled:opacity-50 da base) para impedir submits duplicados/idempotência em formulários. Funciona mesmo em ações síncronas.

Feedback de resultado

Ao resolver a ação assíncrona, faz…

Ao resolver a ação assíncrona, faz uma troca transitória de ícone/rótulo (check + 'Feito'/'Copiado' ou x + 'Falhou') na cor atual da base e reverte — sem verde/vermelho, que seriam decisão de base.

Contagem regressiva (reenvio)

Após o clique, desabilita e mostra…

Após o clique, desabilita e mostra 'Reenviar em Ns' contando até liberar de novo — o padrão clássico de reenviar código/OTP. Conteúdo aditivo no rótulo + comportamento.

Atalho de teclado

Vincula uma tecla/combinação (ex.: mod+s, Enter)…

Vincula uma tecla/combinação (ex.: mod+s, Enter) que dispara o clique do botão e, opcionalmente, exibe a dica com o componente Kbd. Comportamento + composição aditiva. posicionável

Indicador de menu

Marca o botão como gatilho de…

Marca o botão como gatilho de menu: acrescenta um chevron no fim e alterna aria-expanded (estilo que a base já prevê) ao abrir/fechar a lista suspensa. Affordance de disclosure.
Código gerado

Cole no Claude Code — ele acerta de primeira.

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

BASE CONGELADA (não mude por instância): Congelados: o formato de pílula (rounded-lg, e rounded-md nas escalas menores), a paleta das 6 variantes (default/secondary/destructive/outline/ghost/link), a escala de 8 tamanhos (default/xs/sm/lg + icon-*), a tipografia (text-sm font-medium), gap/padding por tamanho, o anel de foco (focus-visible ring), o micro-afundamento no active (translate-y-px), disabled:opacity-50, o auto-size do ícone (size-4), o tratamento aria-invalid, o comportamento RTL/LTR e os atributos semânticos data-slot/data-variant/data-size. Ligar/desligar qualquer capacidade não toca em nada disso.

USO: <twig:Button variant="default" />

CAPACIDADES OPT-IN (ligue sem alterar o visual):
- Estado de carregamento: Enquanto a ação está em voo, troca o conteúdo por um Spinner (componente existente), marca aria-busy e bloqueia o re-clique; volta sozinho ao concluir. [select → Spinner substitui o ícone / Spinner + texto de espera / Só spinner (rótulo oculto)]- Confirmação antes de agir: Intercepta o clique e exige uma confirmação antes de disparar a ação — útil sobretudo com a variante destructive. Puro comportamento, sem alterar o botão. [select → Diálogo de confirmação / Segurar para confirmar / Clicar duas vezes / Confirm nativo do navegador]- Prevenção de duplo envio: Após o clique, desabilita o botão por N milissegundos (reusa disabled:opacity-50 da base) para impedir submits duplicados/idempotência em formulários. Funciona mesmo em ações síncronas. [number]- Feedback de resultado: Ao resolver a ação assíncrona, faz uma troca transitória de ícone/rótulo (check + 'Feito'/'Copiado' ou x + 'Falhou') na cor atual da base e reverte — sem verde/vermelho, que seriam decisão de base. [select → Ícone (check / x) / Ícone + texto / Só texto]- Contagem regressiva (reenvio): Após o clique, desabilita e mostra 'Reenviar em Ns' contando até liberar de novo — o padrão clássico de reenviar código/OTP. Conteúdo aditivo no rótulo + comportamento. [number]- Ícone (posição início/fim): Adiciona um ícone no slot que a própria base já reserva (data-icon=inline-start/inline-end, com o padding correto). Escolhe-se apenas onde ele aparece. [position → Início (inline-start) / Fim (inline-end)]- Badge de contagem: Sobrepõe um Badge numérico (notificações, itens no carrinho) posicionado sobre o botão sem reflow — composição aditiva, não altera o visual da base do Button. [number]- Atalho de teclado: Vincula uma tecla/combinação (ex.: mod+s, Enter) que dispara o clique do botão e, opcionalmente, exibe a dica com o componente Kbd. Comportamento + composição aditiva. [text]- Indicador de menu: Marca o botão como gatilho de menu: acrescenta um chevron no fim e alterna aria-expanded (estilo que a base já prevê) ao abrir/fechar a lista suspensa. Affordance de disclosure. [toggle]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): Congelados: o formato de pílula (rounded-lg, e rounded-md nas escalas menores), a paleta das 6 variantes (default/secondary/destructive/outline/ghost/link), a escala de 8 tamanhos (default/xs/sm/lg + icon-*), a tipografia (text-sm font-medium), gap/padding por tamanho, o anel de foco (focus-visible ring), o micro-afundamento no active (translate-y-px), disabled:opacity-50, o auto-size do ícone (size-4), o tratamento aria-invalid, o comportamento RTL/LTR e os atributos semânticos data-slot/data-variant/data-size

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

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

Spec machine-readable (JSON)

{
    "$schema_version": "1.0",
    "id": "button",
    "component": "Button",
    "eixo": "ui",
    "particularidade": "O Button é o gatilho de ação: sua identidade é o clique e o ciclo de vida do que ele dispara (intenção → confirmação → em-voo → resultado → cooldown). Por isso seus enriquecedores governam esse ciclo e as affordances do gatilho, nunca a aparência.",
    "base_congelada": "Congelados: o formato de pílula (rounded-lg, e rounded-md nas escalas menores), a paleta das 6 variantes (default/secondary/destructive/outline/ghost/link), a escala de 8 tamanhos (default/xs/sm/lg + icon-*), a tipografia (text-sm font-medium), gap/padding por tamanho, o anel de foco (focus-visible ring), o micro-afundamento no active (translate-y-px), disabled:opacity-50, o auto-size do ícone (size-4), o tratamento aria-invalid, o comportamento RTL/LTR e os atributos semânticos data-slot/data-variant/data-size. Ligar/desligar qualquer capacidade não toca em nada disso.",
    "props": [
        {
            "name": "variant",
            "type": "'default'|'secondary'|'destructive'|'outline'|'ghost'|'link'",
            "default": "default",
            "description": "The visual style variant. Defaults to `default` #}"
        },
        {
            "name": "size",
            "type": "'default'|'xs'|'sm'|'lg'|'icon'|'icon-xs'|'icon-sm'|'icon-lg'",
            "default": "default",
            "description": "The button size. Defaults to `default` #}"
        },
        {
            "name": "as",
            "type": "'button'",
            "default": "button",
            "description": "The HTML tag to render. Defaults to `button` #}"
        }
    ],
    "capacidades": [
        {
            "id": "estado-de-carregamento",
            "nome": "Estado de carregamento",
            "descricao": "Enquanto a ação está em voo, troca o conteúdo por um Spinner (componente existente), marca aria-busy e bloqueia o re-clique; volta sozinho ao concluir.",
            "controle": "select",
            "opcoes": [
                "Spinner substitui o ícone",
                "Spinner + texto de espera",
                "Só spinner (rótulo oculto)"
            ],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "confirma-o-antes-de-agir",
            "nome": "Confirmação antes de agir",
            "descricao": "Intercepta o clique e exige uma confirmação antes de disparar a ação — útil sobretudo com a variante destructive. Puro comportamento, sem alterar o botão.",
            "controle": "select",
            "opcoes": [
                "Diálogo de confirmação",
                "Segurar para confirmar",
                "Clicar duas vezes",
                "Confirm nativo do navegador"
            ],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "preven-o-de-duplo-envio",
            "nome": "Prevenção de duplo envio",
            "descricao": "Após o clique, desabilita o botão por N milissegundos (reusa disabled:opacity-50 da base) para impedir submits duplicados/idempotência em formulários. Funciona mesmo em ações síncronas.",
            "controle": "number",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (number). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "feedback-de-resultado",
            "nome": "Feedback de resultado",
            "descricao": "Ao resolver a ação assíncrona, faz uma troca transitória de ícone/rótulo (check + 'Feito'/'Copiado' ou x + 'Falhou') na cor atual da base e reverte — sem verde/vermelho, que seriam decisão de base.",
            "controle": "select",
            "opcoes": [
                "Ícone (check / x)",
                "Ícone + texto",
                "Só texto"
            ],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "contagem-regressiva-reenvio",
            "nome": "Contagem regressiva (reenvio)",
            "descricao": "Após o clique, desabilita e mostra 'Reenviar em Ns' contando até liberar de novo — o padrão clássico de reenviar código/OTP. Conteúdo aditivo no rótulo + comportamento.",
            "controle": "number",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (number). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "cone-posi-o-in-cio-fim",
            "nome": "Ícone (posição início/fim)",
            "descricao": "Adiciona um ícone no slot que a própria base já reserva (data-icon=inline-start/inline-end, com o padding correto). Escolhe-se apenas onde ele aparece.",
            "controle": "position",
            "opcoes": [
                "Início (inline-start)",
                "Fim (inline-end)"
            ],
            "posicionavel": true,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (position). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "badge-de-contagem",
            "nome": "Badge de contagem",
            "descricao": "Sobrepõe um Badge numérico (notificações, itens no carrinho) posicionado sobre o botão sem reflow — composição aditiva, não altera o visual da base do Button.",
            "controle": "number",
            "opcoes": [],
            "posicionavel": true,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (number). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "atalho-de-teclado",
            "nome": "Atalho de teclado",
            "descricao": "Vincula uma tecla/combinação (ex.: mod+s, Enter) que dispara o clique do botão e, opcionalmente, exibe a dica com o componente Kbd. Comportamento + composição aditiva.",
            "controle": "text",
            "opcoes": [],
            "posicionavel": true,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (text). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        },
        {
            "id": "indicador-de-menu",
            "nome": "Indicador de menu",
            "descricao": "Marca o botão como gatilho de menu: acrescenta um chevron no fim e alterna aria-expanded (estilo que a base já prevê) ao abrir/fechar a lista suspensa. Affordance de disclosure.",
            "controle": "toggle",
            "opcoes": [],
            "posicionavel": false,
            "e_funcao": true,
            "como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/button; comportamento via controller Stimulus. Não altera a base."
        }
    ],
    "snippet_uso": "<twig:Button variant=\"default\" />",
    "exemplo_demo": "<div class=\"flex flex-wrap items-center gap-2 md:flex-row\">\n    <twig:Button variant=\"outline\">Button</twig:Button>\n    <twig:Button variant=\"outline\" size=\"icon\" aria-label=\"Submit\">\n        <twig:ux:icon name=\"lucide:arrow-up\" />\n    </twig:Button>\n</div>",
    "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": "Congelados: o formato de pílula (rounded-lg, e rounded-md nas escalas menores), a paleta das 6 variantes (default/secondary/destructive/outline/ghost/link), a escala de 8 tamanhos (default/xs/sm/lg + icon-*), a tipografia (text-sm font-medium), gap/padding por tamanho, o anel de foco (focus-visible ring), o micro-afundamento no active (translate-y-px), disabled:opacity-50, o auto-size do ícone (size-4), o tratamento aria-invalid, o comportamento RTL/LTR e os atributos semânticos data-slot/data-variant/data-size",
    "mcp": {
        "tool": "get_component",
        "args": {
            "id": "button"
        }
    }
}