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
Sete, e a escolha entre elas é de HIERARQUIA, não de gosto: uma tela tem no máximo uma ação primária à vista.
| 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:ring-ia/50 |
| 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:bg-ink-50 |
| 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. | hover:bg-ink-100 |
| 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
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.
| 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.
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-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&_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. |
| disabled:opacity-50 + pointer-events-none | Desabilitado não recebe hover nem clique — e continua legível. |
| 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" | "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. |
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.
| 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 disabled enquanto isso. |
| 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). |