Skeleton
ProntoPlaceholder animado durante carregamento. Reproduz o shape do conteúdo final para evitar layout shift.
Usar quando
Loading inicial de listas, cards, tabela. Async de dados que demora >300ms. Streaming de conteúdo.
Não usar quando
Loading instantâneo (<300ms — só piscaria). Spinner indeterminate (use Spinner). Form submit (use Button loading).
Variantes
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Shimmer | mount | Gradiente brass-tint move da esquerda pra direita continuamente | 1500ms ease-in-out infinite |
| Fade out (replace) | dados chegam | Skeleton fade-out + conteúdo real fade-in | 200ms cross |
| Soft pulse fallback | reduced-motion | Opacity pulse 1→0.5→1 em vez de shimmer | 1500ms loop |
Acessibilidade
Acessibilidade — checklist
ARIA esperado
- aria-busy="true" no container pai
- role="status" + aria-label="Carregando..."
- aria-live="polite" para anunciar quando dados chegam
Notas
- Reproduzir shape exato do conteúdo final (largura, altura, alinhamento) — evita layout shift.
- Em reduced-motion, use pulse em vez de shimmer.
- Não use skeleton para loading curto (<300ms) — apenas pisca.
- Combine com Suspense (React) ou loading.tsx (Next.js).
Código
'use client';
import { T, RADIUS } from '@/lib/tokens';
interface SkeletonProps {
width?: number | string;
height?: number | string;
radius?: 'sm' | 'md' | 'lg' | 'full';
}
export function Skeleton({ width = '100%', height = 14, radius = 'sm' }: SkeletonProps) {
const r = radius === 'full' ? 980 : radius === 'lg' ? RADIUS.lg : radius === 'md' ? RADIUS.md : RADIUS.sm;
return (
<div
role="status"
aria-label="Carregando"
style={{
width, height,
borderRadius: r,
background: `linear-gradient(90deg, ${T.surface3} 0%, ${T.surface2} 50%, ${T.surface3} 100%)`,
backgroundSize: '200% 100%',
animation: 'lb-shimmer 1500ms ease-in-out infinite',
}}
/>
);
}Regras
Faça
- ✓Reproduza shape exato do conteúdo (mesma largura, altura, gap).
- ✓Shimmer brass-tint sutil da esquerda pra direita (1500ms loop).
- ✓Em reduced-motion: pulse em opacity em vez de shimmer.
- ✓aria-busy + aria-label="Carregando" no container.
- ✓Show só após 300ms de delay (evita flash).
Não faça
- ✗Não use spinner em vez de skeleton para listas (skeleton é melhor UX).
- ✗Não force animação agressiva (shimmer rápido cansa).
- ✗Não esqueça de remover quando dados chegarem (cross-fade).
- ✗Não use cor brass forte (deve ser sutil).
- ✗Não use forma genérica de skeleton (deve parecer com o conteúdo final).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.surface3 | #F3F0E6 | cor base do skeleton |
T.surface2 | #FAF8F2 | cor de highlight do shimmer |
RADIUS.sm | 6 | borderRadius default |
RADIUS.pill | 980 | radius=full (avatar) |