Button

Pronto

Dispara 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.

4 variantes em ordem de peso visual

Tamanhos

3 tamanhos. Default é md (36px de altura).

sm 28px · md 36px · lg 44px

Com ícone

Ícone à esquerda do label. Use lucide-react.

Ícone esquerda (default) ou direita (em CTAs de continuidade)

Estados

Todo botão tem 5 estados. Hover/focus em Apple ease 150ms.

Default
Hover (passe o mouse)
Focus (use Tab)
Loading
Disabled

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çãoDisparada porComportamentoTiming
Hover backgroundmouseenterBackground escurece (ink → ink-2)150ms ease
Active pressmousedownScale 0.98 + opacity 0.9100ms ease-out
Focus ringfocus-visible (Tab)Outline 2px preto puro com offset 2pxinstant (sem fade)
Loading spinnerisLoading=trueTexto vira "Salvando…", spinner gira ao lado600ms linear (spinner)
Success morphmutation.onSuccess (opcional)Ícone vira ✓ por 1s, depois reset250ms ease in/out
Disabled cursordisabled=trueOpacity 0.5, cursor not-allowedinstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabMove foco para o botão
Enter / SpaceAciona 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

TokenValorPapel
T.ink#0A0A0Abackground do primary
T.surface#FFFFFFtexto sobre primary
T.border#E3DFD2borda do secondary
T.carmim#A32E2Ebackground do destructive
RADIUS.pill980borderRadius (pílula)
TYPE.sm12fontSize do label
WEIGHT.medium500fontWeight do label
MOTION.fast150msduração do hover
MOTION.easecubic-bezier(0.32, 0.72, 0, 1)curva Apple