Button
Em andamentoO botão do sistema: executa uma ação no lugar — enviar, confirmar, abrir — ou, com href, vira um link com a mesma aparência. Tem oito variantes, três tamanhos e um estado de carregando que não tira o foco de quem apertou.
- Função
- Formulário e ação
- Também chamado de
- botão, CTA, chamada para ação, link de botão
- Mora em
- app/components/atoms/Button.tsx
- Última mudança
- 2026-09-30
Prévia
A peça ao vivo, vinda da ficha, na largura escolhida. Em larguras fixas, ela roda num quadro do tamanho de uma tela de verdade.
Na largura da página: 0px
Variantes na ficha
Lidas da ficha no build, na ordem do palco. Cada uma abre sozinha, sem o styleguide em volta — é o endereço que o palco carrega.
Quando usar
Para a ação principal e as secundárias de uma tela ou formulário, e para navegar quando o destino precisa do peso visual de um botão ("Inscrever-se").
Faça
- Para a ação de apoio ou de escolha ao lado de uma primária ("Escolher esta turma", "Ver grade"), o
tonal: verde claro, com o fio verde por dentro (E127). Não é osecondary, que é o verde cheio de confirmar. - Escreva verbo e objeto, de uma a três palavras, sem ponto final: "Enviar inscrição".
- Use
hrefpara navegar e o<button>para agir; o componente decide a tag. - Use
isLoadingdurante o envio: o foco fica no botão, que anunciaaria-busye ignora novos cliques. - Passe
type="submit"explicitamente no botão que envia o formulário — o padrão aqui étype="button". - Dê
aria-labelao botão que só tem ícone.
Evite
- Envolver o Button num
<a>: são dois elementos interativos aninhados, e o leitor anuncia os dois. - Desabilitar para dizer que falta algo: botão desabilitado sai do Tab e não explica o motivo. Se precisar, escreva ao lado o que falta.
- Mais de um
variant="primary"na mesma área — a ação principal deixa de ser principal. - Trocar
secondarypor cinza:secondaryé verde desde setembro; o cinza éneutral.
Quando não usar. Para abrir uma lista de ações, use o Dropdown. Para escolher entre opções que ficam marcadas, o FilterGroup (filtro) ou o Radio (formulário). Para um link dentro de um parágrafo, texto com link — ou, no máximo, variant="link".
Nota de uso
variant="secondary" é verde desde a decisão de cor de setembro; o cinza de antes virou variant="neutral". Alvo mínimo de 44×44 px em todos os tamanhos — a única exceção documentada é variant="link", que é texto corrido dentro de um parágrafo. `href` vira link com a mesma aparência: use para navegar, e o <button> para agir — nunca <a><Button /></a>. isLoading não desabilita: o foco fica no botão, que anuncia aria-busy e ignora o clique. Texto: verbo e objeto, de uma a três palavras, sem ponto final — "Enviar inscrição", não "Clique aqui". Evite desabilitar: botão desabilitado não diz o que falta. Se precisar, escreva ao lado o motivo e como destravar.
Anatomia
As partes, na ordem em que o olho — e o teclado — passam.
- Ícone à esquerda — `leftIcon`, opcional e decorativo — o texto já descreve a ação.
- Rótulo — O `children`: verbo e objeto. Com `isLoading`, fica escondido do leitor e o `loadingLabel` é lido no lugar.
- Ícone à direita — `rightIcon`, opcional e decorativo.
- Indicador de carregando — Um `Loader2` que gira só com movimento liberado (`motion-safe:animate-spin`).
Estados
Os estados que a peça tem no código, e o que muda em cada um.
| Estado | O que muda |
|---|---|
| Padrão | Cor da variante; primary por padrão. |
| Hover | Tom mais escuro da variante (hover:bg-primary-dark, hover:bg-secondary-dark…); o tonal vira o verde cheio; link sublinha. |
| Foco | Anel ring-ring de 2px com afastamento de 2px, igual nas oito variantes; só no teclado (focus-visible). |
| Carregando | isLoading: ícone girando, aria-busy="true" e aria-disabled="true", opacidade 50% e cursor de espera. Continua focável; o clique e o envio são bloqueados. |
| Desabilitado | disabled: opacidade 50%, sem ponteiro e fora da ordem de Tab. No modo link (href), disabled vira aria-disabled e o clique é bloqueado. |
Comportamento
| Situação | O que acontece |
|---|---|
| Com `href` | Renderiza next/link com a aparência de botão. Com target="_blank", o rel="noopener noreferrer" vem sozinho. |
| Sem `type` dentro de um formulário | Não envia: o padrão é type="button". Um "Cancelar" não dispara o envio por engano. |
| Clique ou tecla com `isLoading` | Nada acontece — medido: Enter e Espaço não disparam o onClick. |
| Sem texto e sem `aria-label` | Em desenvolvimento, o console avisa: "Button: botão sem texto visível precisa de aria-label." |
| `variant="link"` | Perde a altura mínima de 44px e o preenchimento: é texto dentro de um parágrafo. É a única exceção de alvo documentada. |
Relacionados
Como usar no código
A importação e o menor uso, lidos do arquivo. O … marca as props obrigatórias.
import { Button } from '@/app/components/atoms/Button';
<Button>…</Button>Propriedades
Lidas do código da peça no build — não escritas à mão.
| Prop | Tipo | Padrão | Obrigatória | Descrição |
|---|---|---|---|---|
variant | 'primary' | 'secondary' | 'neutral' | 'outline' | 'ghost' | 'link' | 'success' | 'tonal' | 'destructive' | 'primary' | não | Variante do cva. |
size | 'sm' | 'md' | 'lg' | 'md' | não | Todos os tamanhos têm no mínimo 44px de altura — a trava de alvo de toque do relatório A11Y. O que muda entre eles é o padding horizontal e o tamanho do texto: um botão pequeno continua parecendo pequeno, sem que a área clicável fique abaixo do mínimo. |
fullWidth | boolean | false | não | Variante do cva. |
href | string | — | não | Vira um link (next/link) com a aparência de botão. Use para navegar — "Inscrever-se", "Ver detalhes" —, e o <button> para agir no lugar. Nunca <a><Button /></a>: são dois elementos interativos, um dentro do outro, e o leitor anuncia os dois. |
target | React.AnchorHTMLAttributes<HTMLAnchorElement>['target'] | — | não | Só com href. Com _blank, o rel="noopener noreferrer" vem sozinho. |
rel | string | — | não | — |
isLoading | boolean | false | não | — |
loadingLabel | string | 'Carregando…' | não | Anunciado por leitor de tela enquanto isLoading. Prefira algo específico: "Enviando inscrição…". |
leftIcon | React.ReactNode | — | não | Ícone decorativo à esquerda. Passe aria-hidden nele — o rótulo do botão já descreve a ação. |
rightIcon | React.ReactNode | — | não | Ícone decorativo à direita. Mesma regra do leftIcon. |
Aceita também: React.ButtonHTMLAttributes<HTMLButtonElement>.
Medidas
| Parte | Valor | Classe |
|---|---|---|
| Altura, `sm` e `md` | 44px no mínimo | min-h-alvo-toque |
| Altura, `lg` | 48px no mínimo | min-h-12 |
| Anel de foco | 2px, afastado 2px | focus-visible:ring-2 focus-visible:ring-offset-2 |
Tokens de cor
Os tokens semânticos que o código da peça usa, com o valor de hoje em app/globals.css.
| Token | Valor | Usado como |
|---|---|---|
--destructive | #BA0517 | bg-destructivebg-destructive/90 |
--primary | #D42B2B | bg-primarytext-primary |
--secondary-dark | #0D6B2E | bg-secondary-darktext-secondary-dark |
--border | #E5E5E5 | bg-border |
--border-interactive | #8C8C8C | border-border-interactive |
--card | #FFFFFF | bg-card |
--destructive-foreground | #FFFFFF | text-destructive-foreground |
--foreground | #1A1A1A | text-ink |
--muted | #F4F6F9 | bg-muted |
--muted-foreground | #595959 | text-muted-foreground |
--primary-dark | #A81E1E | bg-primary-dark |
--primary-foreground | #FFFFFF | text-primary-foreground |
--ring | var(--footer-red)), decisão da revisão 2. Se outra peça precisar
* dele como superfície, o nome vira de papel (ex.: --surface-brand-strong). */
--footer-red: #7D1B1B | ring-ring |
--secondary | #0B8434 | bg-secondary |
--secondary-foreground | #FFFFFF | text-secondary-foreground |
--secondary-subtle | #EAF4EE | bg-secondary-subtle |
--success | #2E844A | bg-success |
--success-foreground | #FFFFFF | text-success-foreground |
--success-strong | #276F3E | bg-success-strong |
Teclado
| Tecla | Faz |
|---|---|
| Tab | Leva o foco ao botão. Desabilitado, ele é pulado; carregando, continua na ordem. |
| EnterEspaço | Aciona o botão — medido: cada tecla dispara um clique. No modo link, só Enter segue o link, como em todo link. |
Leitor de tela
O que é anunciado, e por quê.
- Com
isLoading, o nome lido é oloadingLabel("Carregando…" por padrão) — o rótulo visível ficaaria-hidden. Prefira algo específico: "Enviando inscrição…". - Carregando não é
disabled: o botão continua na árvore e com o foco, e anunciaaria-busy.
O que sustenta o estado
Esta peça usa estatico. O estado dela é o de quem já foi ouvido com leitor de tela nesses padrões — e não uma passada sobre esta peça em específico.
Falta ouvir: estatico — ver docs/a11y-screen-reader-checklist.md.
Status e recursos
| Tipo | Recurso | Status |
|---|---|---|
| Código | app/components/atoms/Button.tsx | Disponível |
| Ficha | Storybook (abre em outra aba) | Disponível |
| Teste de acessibilidade | axe em npm run test | No gate de todo commit |
| Leitor de tela | Passada da E33 | Falta ouvir |
| Design | Figma | Desativado até a Fase 2 |
Histórico
Os últimos commits que tocaram o arquivo, lidos do git no build.
0298af82026-09-30E147: fechamento da Entrega 11 — REPORT.md, verificação cross-agent e o gate estático estrito
O histórico completo está no GitHub (abre em outra aba).