Icon Button

Pronto

Botão sem label visível, só ícone. Compacto para toolbar, header, ações por linha. Sempre com aria-label e Tooltip.

Usar quando

Ações secundárias (kebab, edit, delete por linha), ações em barra de ferramentas, toggle (favorito, like).

Não usar quando

Ação primária de página (use Button com label). Ação destrutiva sem confirmação (use Button + Modal). Quando ícone não comunica claramente.

Variantes

Variantes (ghost / subtle / solid)
Tamanhos
Toggle (estado on/off)
Com badge counter
Em grupo (toolbar)

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
HovermouseenterBackground → surfaceHover, ícone ink150ms ease
Active pressmousedownScale 0.95100ms ease
Toggle morphclick em toggleÍcone ganha cor (heart vira carmim, star vira brass)200ms cubic-bezier(0.32,0.72,0,1)
Badge appearcount muda de 0Badge scale 0→1 sobre o ícone250ms
Focus ringTabOutline 2px borderInk com offsetinstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabFoco
Enter / SpaceAciona
ARIA esperado
  • aria-label="..." // OBRIGATÓRIO (sem label visual)
  • aria-pressed="true|false" se toggle
  • aria-busy="true" durante async
Notas
  • Sempre tenha aria-label E Tooltip — ícone-only sem label = a11y FAIL.
  • Não dependa de cor sozinha (favorito ativo: ícone preenche, não só muda cor).
  • Touch target mínimo 44×44 em mobile (mesmo que ícone seja 14px).
  • Em toolbar, separe ações destrutivas com divider.

Código

'use client';
import { Edit3 } from 'lucide-react';
import { T, RADIUS } from '@/lib/tokens';

interface IconBtnProps {
  Icon: React.ComponentType<{ size?: number }>;
  label: string;  // aria-label OBRIGATÓRIO
  variant?: 'ghost' | 'subtle' | 'solid';
  tone?: 'default' | 'destructive';
  size?: 'xs' | 'sm' | 'md' | 'lg';
  onClick?: () => void;
}

const SIZES = { xs: 24, sm: 28, md: 32, lg: 40 };

export function IconBtn({ Icon, label, variant = 'ghost', tone = 'default', size = 'md', onClick }: IconBtnProps) {
  const px = SIZES[size];
  return (
    <button
      type="button" onClick={onClick} aria-label={label}
      style={{
        width: px, height: px, borderRadius: 8,
        background: variant === 'solid' ? T.ink : 'transparent',
        border: variant === 'subtle' ? `1px solid ${T.border}` : 'none',
        color: variant === 'solid' ? T.surface : tone === 'destructive' ? T.carmim : T.inkMuted,
        cursor: 'pointer', display: 'inline-grid', placeItems: 'center',
        transition: 'all 150ms ease',
      }}
    >
      <Icon size={px * 0.45} />
    </button>
  );
}

Regras

Faça

  • aria-label SEMPRE obrigatório.
  • Tooltip SEMPRE acompanha (descobre função em hover).
  • Touch target mínimo 32px em desktop, 44px em mobile.
  • Variantes: ghost (default), subtle (com borda), solid (destacado).
  • Toggle: ícone outline → preenchido + cor.

Não faça

  • Não use icon-only sem aria-label (FAIL absoluto).
  • Não use icon-only sem Tooltip (descoberta).
  • Não use ícone que não comunica claramente.
  • Não force ícone < 12px (ilegível).
  • Não tenha 5+ icons agrupados sem separador.

Tokens usados

TokenValorPapel
T.ink#0A0A0Avariant solid
T.surface#FFFFFFícone sobre solid
T.surfaceHover#F7F5EChover ghost
T.border#E3DFD2borda subtle
T.inkMuted#5A5A5Eícone default
T.carmim#A32E2Etone destructive
RADIUS.sm/md6/12borderRadius