Componentes
Botão
rounded-full. Sete variantes, sete tamanhos, e cada um sobe um degrau em aparelho de dedo, sozinho.Variantes
| Variante | Quando | Receita |
|---|---|---|
| default | A ação principal da tela ou do bloco. Uma por cena. | bg-primary text-primary-foreground hover:bg-brand-strong |
| ai | Açã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-soft | O 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 |
| outline | Açã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 |
| secondary | Ação neutra dentro de um bloco que já tem primária. | bg-secondary text-secondary-foreground hover:bg-secondary/80 |
| ghost | Açã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 |
| negative | Gatilho 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 |
| destructive | Só depois de confirmação. Nunca como gatilho direto de exclusão. | bg-destructive text-destructive-fg hover:bg-destructive/90 |
| link | Navegaçã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
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
| size | Altura | No dedo | Onde |
|---|---|---|---|
| xs | 24px | 32px | Chip de ação dentro de uma linha densa |
| sm | 32px | 40px | Toolbar, linha de lista, rodapé de card |
| default | 40px | 44px | CTA da tela, ação de modal |
| lg | 40px | 44px | Igual ao default com px-6 — CTA que precisa de mais peso |
| icon-xs | 24px | 32px | Ícone solo em linha densa |
| icon-sm | 32px | 40px | Kebab, fechar, ações de linha |
| icon | 40px | 44px | Í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.
| size | Ícone lidera | Ícone fecha | Sem classificar |
|---|---|---|---|
| xs | 6 / 10 | 10 / 6 | 8 / 8 |
| sm | 8 / 12 | 12 / 8 | 10 / 10 |
| default | 12 / 16 | 16 / 12 | 14 / 14 |
| lg | 14 / 18 | 18 / 14 | 16 / 16 |
NotaQuem sabe de que lado o ícone está
<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
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
| Detalhe | Por quê |
|---|---|
| transition-[…] em vez de transition-all | A 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-px | 1px pra baixo no clique. É o único “peso” do botão — não há sombra pressionada. |
| shrink-0 | Botão dentro de flex não amassa antes do texto ao lado. |
| whitespace-nowrap | Rótulo de botão não quebra em duas linhas: se não cabe, o rótulo é longo demais. |
| [&_svg]:size-4 condicional | O í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-none | Desabilitado não recebe hover nem clique — e continua legível. |
| aria-disabled: a mesma cara, segue focável | Pro 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-invalid | Um botão pode ser inválido (o submit de um form com erro): a borda acompanha. |
| Prop | Tipo | Padrão | O 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. |
| asChild | boolean | false | Renderiza 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. |
| …props | React.ComponentProps<'button'> | — | type, disabled, onClick, form, aria-* — tudo do elemento nativo. |
<Button asChild> <Link href="/cadastros"><Users /> Ver cadastros</Link> </Button>
Regras de uso
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.
| Regra | Detalhe |
|---|---|
| Rótulo é verbo no infinitivo + objeto | “Confirmar consulta”, “Enviar proposta”, “Tentar de novo”. Nunca “OK”, “Submeter”, “Confirmação De Consulta”. |
| Sentence case | Só 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-label | Com 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 clique | Exclusão passa por ConfirmStrip ou ConfirmDialog. |
| Cancelar antes de Confirmar | No desktop, lado a lado à direita; no celular empilhados em flex-col-reverse (o principal embaixo, no polegar). |