Pular para o conteúdo
Botão

Componentes

Botão

Pílula em todos os tamanhos — botão é a coisa que se TOCA, e no vocabulário de forma isso é rounded-full. Sete variantes, sete tamanhos, e cada um sobe um degrau em aparelho de dedo, sozinho.
src/components/ui/button.tsxcvaradix Slot (asChild)

Variantes

Nove, e a escolha entre elas é de HIERARQUIA, não de gosto: uma tela tem no máximo uma ação primária à vista.
default · ai · ai-soft · outline · secondary · ghost · negative · destructive · link
VarianteQuandoReceita
defaultA ação principal da tela ou do bloco. Uma por cena.bg-primary text-primary-foreground hover:bg-brand-strong
aiAção que dispara o agente. Sempre com Sparkles.bg-ia text-ia-fg hover:bg-ia-strong focus-visible:border-ia focus-visible:ring-ia-muted
ai-softO mesmo território, com metade da voz: o gatilho da IA que mora DENTRO de uma peça (o painel de variáveis do modelo de contrato), onde o roxo cheio viraria o elemento mais forte da tela num canto em que ele nem age.bg-ia-muted text-ia-strong hover:brightness-95 focus-visible:border-ia focus-visible:ring-ia-muted
outlineAção secundária, e o gatilho de algo que precisa parecer clicável (o Cancelar de um par, “Aplicar preço”).border bg-surface hover:border-border-strong hover:bg-ink-100
secondaryAção neutra dentro de um bloco que já tem primária.bg-secondary text-secondary-foreground hover:bg-secondary/80
ghostAção de linha e de toolbar, onde a borda seria ruído. Cuidado: sem borda, ele pode não parecer clicável. Por isso o hover sobe pro tom da borda.hover:bg-border
negativeGatilho de ação com consequência: Sair, Desativar, Desconectar, Remover, Excluir, Cancelar contrato. Só texto vermelho, rosado no hover. Cancelar e Fechar que só desistem NÃO usam: vermelho ao lado do Salvar mente o peso.text-negative-strong hover:bg-negative-soft focus-visible:border-negative focus-visible:ring-negative-soft
destructiveSó depois de confirmação. Nunca como gatilho direto de exclusão.bg-destructive text-destructive-fg hover:bg-destructive/90
linkNavegação escrita dentro de um texto ou no fim de uma lista.text-primary underline-offset-4 hover:underline

Armadilha já pagaGhost pode esconder o clique

O gatilho de “Aplicar R$ X” era ghost e lia como texto solto — o founder só descobriu que era clicável quando avisado. Virou outline: a borda cinza serve aos dois pesos, e o vermelho continua morando na tarja de confirmação, onde a consequência está escrita.

Tamanhos

Três de texto e três de ícone, mais o lg (que é o default com mais respiro lateral). Não existem tamanhos intermediários.
xs h-6 · sm h-8 (toolbar) · default h-10 (CTA) · lg h-10 com px-6
icon-xs 24 · icon-sm 32 · icon 40 — e o estado desabilitado
sizeAlturaNo dedoOnde
xs24px32pxChip de ação dentro de uma linha densa
sm32px40pxToolbar, linha de lista, rodapé de card
default40px44pxCTA da tela, ação de modal
lg40px44pxIgual ao default com px-6 — CTA que precisa de mais peso
icon-xs24px32pxÍcone solo em linha densa
icon-sm32px40pxKebab, fechar, ações de linha
icon40px44pxÍcone solo como ação principal

O padding lateral encolhe quando há ícone (has-[>svg]:px-3.5): ícone tem menos peso óptico que letra, e sem esse ajuste o botão com ícone fica visualmente mais largo que o irmão sem.

E ele encolhe SÓ DE UM LADO. A caixa do svg tem 16px, mas o glifo do lucide ocupa ~10,7px dentro dela: sobram ~2,6px de ar que ninguém vê. Com padding igual dos dois lados esse ar soma só do lado da borda, e o conteúdo parece empurrado pro lado contrário ao ícone. A folga desloca 2px pro lado certo e a largura não muda — no sm, 8 + 12 é o mesmo total que 10 + 10.

ícone que lidera e ícone que fecha — a folga espelha
sizeÍcone lideraÍcone fechaSem classificar
xs6 / 1010 / 68 / 8
sm8 / 1212 / 810 / 10
default12 / 1616 / 1214 / 14
lg14 / 1818 / 1416 / 16

NotaQuem sabe de que lado o ícone está

Quem decide o lado é o COMPONENTE, não o CSS. Num <Plus />Texto o texto é text node, não elemento: o svg casa :first-child e :last-child ao mesmo tempo, nos dois sentidos. Então o Button lê os filhos (descendo nos fragmentos, e no filho do asChild) e escreve data-icone="lider" ou "fecha"; "ambos" é ícone dos dois lados ou ícone sozinho, e fica simétrico. O has-[>svg] continua como piso: o seletor de atributo tem especificidade maior que o :has(), então a folga vence sem depender da ordem do CSS.

Anatomia e detalhes

A classe base do botão carrega sete decisões. Nenhuma é decorativa.
ui/button.tsx — a base do cva
inline-flex shrink-0 items-center justify-center gap-2
rounded-full text-sm font-medium whitespace-nowrap
transition-[color,background-color,border-color,box-shadow,opacity]
outline-none focus-visible:border-brand focus-visible:ring-[3px] focus-visible:ring-brand-muted
active:translate-y-px
disabled:pointer-events-none disabled:opacity-50
aria-disabled:pointer-events-none aria-disabled:opacity-50
aria-invalid:border-destructive aria-invalid:ring-negative-soft
[&_svg]:pointer-events-none [&_svg]:shrink-0
[&_svg:not([class*='size-'])]:size-4
DetalhePor quê
transition-[…] em vez de transition-allA lista exclui o transform de propósito: o active:translate-y-px é resposta tátil imediata, e animá-lo atrasa o toque.
active:translate-y-px1px pra baixo no clique. É o único “peso” do botão — não há sombra pressionada.
shrink-0Botão dentro de flex não amassa antes do texto ao lado.
whitespace-nowrapRótulo de botão não quebra em duas linhas: se não cabe, o rótulo é longo demais.
[&_svg]:size-4 condicionalO ícone herda o tamanho do botão, e um className explícito no ícone vence.
cursor: pointer (globals.css)A mãozinha mora numa regra só da camada base: todo button e todo role="button" que não está desabilitado. O preflight do Tailwind 4 a tirou; a peça não precisa pedir cursor-pointer, e um cursor-* na peça ainda vence.
disabled:opacity-50 + pointer-events-noneDesabilitado não recebe hover nem clique — e continua legível.
aria-disabled: a mesma cara, segue focávelPro botão que para com o foco nele (a gravação em curso, a ponta de Subir/Descer): disabled tiraria o foco de quem usa o teclado. Quem usa também ignora o clique no handler.
aria-invalidUm botão pode ser inválido (o submit de um form com erro): a borda acompanha.
PropTipoPadrãoO que faz
variant"default" | "ai" | "outline" | "secondary" | "ghost" | "negative" | "destructive" | "link""default"A hierarquia da ação.
size"xs" | "sm" | "default" | "lg" | "icon" | "icon-xs" | "icon-sm""default"Cresce sozinho em pointer-coarse.
asChildbooleanfalseRenderiza o filho no lugar do <button> mantendo as classes — o jeito certo de fazer um <Link> parecer botão sem aninhar âncora dentro de botão.
…propsReact.ComponentProps<'button'>—type, disabled, onClick, form, aria-* — tudo do elemento nativo.
link com cara de botão
<Button asChild>
  <Link href="/cadastros"><Users /> Ver cadastros</Link>
</Button>

Regras de uso

O que a revisão cobra num botão novo.

Sim

Ação que dispara o agente: roxa, com Sparkles, foco roxo.

Não

Ação de IA em verde confunde a convenção “verde = ação direta, roxo = agente”.

Sim

Uma primária por cena, e ela fica por último (mais perto do canto onde o olho termina). Verbo no infinitivo + objeto.

Não

Três primárias na mesma linha: nenhuma é primária. E “Exportar” sem ícone quebra o ritmo do cluster.

RegraDetalhe
Rótulo é verbo no infinitivo + objeto“Confirmar consulta”, “Enviar proposta”, “Tentar de novo”. Nunca “OK”, “Submeter”, “Confirmação De Consulta”.
Sentence caseSó a primeira letra maiúscula, sempre.
Ícone à esquerdaÍcone à direita só quando ele indica direção (a seta de “próximo”).
Botão só de ícone precisa de aria-labelCom o verbo e o objeto: “Excluir Marina Alvez”.
Estado de espera troca o rótulo“Salvando…”, “Excluindo…” — e o botão fica parado enquanto isso: disabled, ou aria-disabled quando o foco pode estar nele.
Nunca destructive como primeiro cliqueExclusão passa por ConfirmStrip ou ConfirmDialog.
Cancelar antes de ConfirmarNo desktop, lado a lado à direita; no celular empilhados em flex-col-reverse (o principal embaixo, no polegar).