Icon Button
ProntoBotã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
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Hover | mouseenter | Background → surfaceHover, ícone ink | 150ms ease |
| Active press | mousedown | Scale 0.95 | 100ms ease |
| Toggle morph | click em toggle | Ícone ganha cor (heart vira carmim, star vira brass) | 200ms cubic-bezier(0.32,0.72,0,1) |
| Badge appear | count muda de 0 | Badge scale 0→1 sobre o ícone | 250ms |
| Focus ring | Tab | Outline 2px borderInk com offset | instant |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Foco |
| Enter / Space | Aciona |
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
| Token | Valor | Papel |
|---|---|---|
T.ink | #0A0A0A | variant solid |
T.surface | #FFFFFF | ícone sobre solid |
T.surfaceHover | #F7F5EC | hover ghost |
T.border | #E3DFD2 | borda subtle |
T.inkMuted | #5A5A5E | ícone default |
T.carmim | #A32E2E | tone destructive |
RADIUS.sm/md | 6/12 | borderRadius |