Button
ProntoDispara uma ação. Quatro variantes: primary, secondary, ghost, destructive.
Usar quando
Sempre que houver uma ação executável (criar, salvar, enviar, navegar). Use primary para a ação principal de cada viewport — apenas uma.
Não usar quando
Para navegação cosmética sem ação real. Use Link nativo. Para múltiplas ações de igual peso, considere segmented control ou dropdown.
Variantes
Apenas uma primary por viewport. Use ghost para ações terciárias.
Tamanhos
3 tamanhos. Default é md (36px de altura).
Com ícone
Ícone à esquerda do label. Use lucide-react.
Estados
Todo botão tem 5 estados. Hover/focus em Apple ease 150ms.
Código
Copie o snippet da variante desejada.
import { Plus } from 'lucide-react';
import { T, TYPE, WEIGHT, SP, RADIUS, MOTION, transition } from '@/lib/tokens';
// Primary — única na viewport
<button
style={{
display: 'inline-flex',
alignItems: 'center',
gap: SP[1],
height: 36,
paddingLeft: SP[4],
paddingRight: SP[4],
borderRadius: RADIUS.pill,
background: T.ink,
color: T.surface,
fontSize: TYPE.sm,
fontWeight: WEIGHT.medium,
boxShadow: T.shadowSm,
transition: transition('background-color', 'fast'),
}}
onMouseEnter={(e) => (e.currentTarget.style.background = T.ink2)}
onMouseLeave={(e) => (e.currentTarget.style.background = T.ink)}
>
<Plus className="h-3.5 w-3.5" />
Criar área
</button>Microinterações
Toda mudança de estado tem motion. Apple ease em todas.
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Hover background | mouseenter | Background escurece (ink → ink-2) | 150ms ease |
| Active press | mousedown | Scale 0.98 + opacity 0.9 | 100ms ease-out |
| Focus ring | focus-visible (Tab) | Outline 2px preto puro com offset 2px | instant (sem fade) |
| Loading spinner | isLoading=true | Texto vira "Salvando…", spinner gira ao lado | 600ms linear (spinner) |
| Success morph | mutation.onSuccess (opcional) | Ícone vira ✓ por 1s, depois reset | 250ms ease in/out |
| Disabled cursor | disabled=true | Opacity 0.5, cursor not-allowed | instant |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Move foco para o botão |
| Enter / Space | Aciona o botão |
| Esc | (em modal) Fecha sem submeter |
ARIA esperado
- aria-label="..." // obrigatório se ícone-only
- aria-busy="true" // durante loading
- aria-disabled="true" // se disabled (não use disabled em <a>)
Notas
- Botão de ícone-only sem aria-label = bug crítico (screen reader fala "botão").
- Loading state preserva o foco — não muda DOM, só conteúdo interno.
- Contraste mínimo 4.5:1 (primary preto sobre branco passa folgado).
Regras
Faça
- ✓Apenas uma primary por viewport — a ação dominante.
- ✓Use ghost para ações terciárias (link "Saiba mais", "Cancelar" em modal).
- ✓Sempre tenha aria-label se o botão for ícone-only.
- ✓Use destructive (carmim) só em ações que destroem dados (excluir, cancelar contrato).
- ✓Loading state troca o label por "Salvando…" + opacity 0.7.
Não faça
- ✗Não use brass como background de botão. Brass é sinal, não acento de ação.
- ✗Não tenha 2+ primaries competindo na mesma tela.
- ✗Não troque "Salvar" por "OK" ou "Confirmar" — diga o verbo de ação real.
- ✗Não use cor para "ficar bonito" — só destructive (carmim) tem cor além de preto.
- ✗Não esqueça transition no hover (jumpy).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.ink | #0A0A0A | background do primary |
T.surface | #FFFFFF | texto sobre primary |
T.border | #E3DFD2 | borda do secondary |
T.carmim | #A32E2E | background do destructive |
RADIUS.pill | 980 | borderRadius (pílula) |
TYPE.sm | 12 | fontSize do label |
WEIGHT.medium | 500 | fontWeight do label |
MOTION.fast | 150ms | duração do hover |
MOTION.ease | cubic-bezier(0.32, 0.72, 0, 1) | curva Apple |