Card
ProntoContainer de uma unidade de informação coesa. Sombra sutil, radius generoso, padding interno generoso. Reusa em KPIs, processos, cards do Kanban.
Usar quando
Agrupar campos relacionados (KPI, processo, contato). Item em grid. Surface elevada para destaque visual.
Não usar quando
Container sem identidade própria (use <div>). Quando viola hierarquia (cards aninhados em cards são suspeitos).
Variantes
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Hover lift (interactive) | mouseenter | translateY -2px + sombra cresce sm→md | 200ms cubic-bezier(0.32,0.72,0,1) |
| Click press | mousedown (interactive) | translateY 0 + scale 0.99 | 100ms ease |
| Focus ring | focus-visible (interactive) | outline 2px borderInk com offset 2px | instant |
| Stat delta pulse | value muda | Delta scale 1→1.1→1 | 300ms ease |
Acessibilidade
Acessibilidade — checklist
ARIA esperado
- Card interativo: <a> ou <button> wrapping content
- Card de display: <article> ou <div role="region" aria-labelledby>
- Header: <h3> dentro do card
- Ações no header: <button aria-label="Mais opções" />
Notas
- Cards aninhados são red flag — repensar hierarquia.
- Card interativo precisa ser <a> ou <button> (não <div onClick>).
- Padding generoso (24px) > apertado (12px). Branco é a feature mais cara.
- Elevation 1 default, 2 só pra destacar (hero, modal).
Código
'use client';
import { T, SP, RADIUS } from '@/lib/tokens';
interface CardProps {
children: React.ReactNode;
elevation?: 1 | 2 | 3;
outline?: boolean;
interactive?: boolean;
padding?: number;
}
export function Card({ children, elevation = 1, outline, interactive, padding = SP[5] }: CardProps) {
const shadow = outline ? 'none' : elevation === 1 ? T.shadowSm : elevation === 2 ? T.shadowMd : T.shadowLg;
return (
<div
style={{
background: T.surface,
borderRadius: RADIUS.lg,
boxShadow: shadow,
border: outline ? `1px solid ${T.border}` : 'none',
padding,
cursor: interactive ? 'pointer' : 'default',
transition: interactive ? 'all 200ms cubic-bezier(0.32,0.72,0,1)' : 'none',
}}
onMouseEnter={(e) => {
if (interactive) {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = T.shadowMd;
}
}}
onMouseLeave={(e) => {
if (interactive) {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = shadow;
}
}}
>
{children}
</div>
);
}Regras
Faça
- ✓Padding interno generoso (SP[5] ou SP[6] = 20-24px).
- ✓Radius lg (18px) — Apple usa 14-22px.
- ✓Elevation 1 default; 2 só para destaque; 3 só para overlay.
- ✓Interactive card precisa ser <a> ou <button> (semântica + a11y).
- ✓Header opcional: <h3> + ações à direita (kebab).
Não faça
- ✗Não aninhe cards (cards dentro de cards = hierarquia quebrada).
- ✗Não use border + sombra simultâneos (pollui visual).
- ✗Não force radius pequeno (sm = 6px é só para chips/badges).
- ✗Não tenha mais que 3 níveis de elevation na mesma view.
- ✗Não use cor brass como background de card (brass é sinal).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.surface | #FFFFFF | background |
T.shadowSm | 0 1px 2px ... | elevation 1 |
T.shadowMd | 0 4px 14px ... | elevation 2 + hover |
T.shadowLg | 0 16px 40px ... | elevation 3 |
T.border | #E3DFD2 | borda outline |
RADIUS.lg | 18 | borderRadius |
SP[5] | 20 | padding interno |