Empty State

Pronto

Mensagem amigável quando uma listagem ou área está vazia. Sempre tem ícone, título, descrição e CTA quando aplicável.

Usar quando

Lista vazia em primeiro acesso (zero state). Resultado de busca/filtro vazio. Acesso negado. Área aguardando dado externo.

Não usar quando

Loading (use Skeleton). Erro de rede (use ErrorState). Lista temporariamente vazia mas com dados em outras abas (mostre número 0 sem empty state cheio).

Variantes

4 contextos cobrem 95% dos casos.

Zero state — primeiro acesso

Nenhuma área cadastrada

Crie a primeira área para categorizar processos e gerar relatórios.

Busca vazia — após filtrar

Nenhum resultado para "trabalhista"

Tente outro termo ou ajuste os filtros aplicados.

Acesso negado

Você não tem acesso a este módulo

Solicite ao administrador do escritório a permissão "financeiro.view".

Aguardando dado externo

Aguardando primeira intimação

Configurações de captura ativas. Próxima execução em 2h.

Anatomia

5 partes; 3 são obrigatórias.

Nenhuma área cadastrada

Crie a primeira área para categorizar processos e gerar relatórios.

1
Ícone (obrigatório)
Lucide icon, 20px, dentro de círculo brass-tint.
2
Título (obrigatório)
Display Source Serif, 21px, semibold.
3
Descrição (obrigatória)
13px sans, ink-muted, ≤2 frases.
4
CTA primário (recomendado)
Button primary com ação imediata.
5
Link secundário (opcional)
Ghost button "Saiba mais".

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Mount fade-incomponente entra no DOMopacity 0→1 + translateY 8px→0 do conjunto250ms cubic-bezier(0.32,0.72,0,1)
Ícone pulse (sutil)mount, depois 1x a cada 8sscale 1→1.05→1 do círculo do ícone600ms ease in-out
CTA hovermouseenter no botão primárioBackground ink → ink-2 (padrão Button)150ms ease

Acessibilidade

Acessibilidade — checklist

ARIA esperado
  • role="status" // se transitório (search vazio depois de filtrar)
  • aria-live="polite" // anuncia "nenhum resultado" para screen reader
Notas
  • Empty state NÃO é um erro — não use cor carmim ou ícone de aviso.
  • Ícone deve ser semanticamente significativo (Inbox para "sem mensagens", FileSearch para "busca vazia").
  • CTA é primary mas NÃO bloqueia (usuário pode ignorar e navegar).
  • Texto descreve a ação ("Crie sua primeira área"), não o problema ("Lista vazia").

Código

'use client';
import { Plus, Scale } from 'lucide-react';
import { T, FONT, TYPE, WEIGHT, SP, RADIUS, transition } from '@/lib/tokens';

interface EmptyStateProps {
  icon: React.ComponentType<{ size?: number }>;
  title: string;
  description: string;
  cta?: { label: string; onClick: () => void; icon?: React.ComponentType };
  link?: { label: string; href: string };
}

export function EmptyState({ icon: Icon, title, description, cta, link }: EmptyStateProps) {
  return (
    <div
      role="status"
      aria-live="polite"
      style={{
        background: T.surface,
        border: `1px dashed ${T.border}`,
        borderRadius: RADIUS.lg,
        padding: `${SP[16]}px ${SP[6]}px`,
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        textAlign: 'center',
        gap: SP[3],
      }}
    >
      <div
        style={{
          width: 48,
          height: 48,
          borderRadius: 980,
          background: T.brassTint,
          color: T.brass,
          display: 'grid',
          placeItems: 'center',
        }}
      >
        <Icon size={20} />
      </div>
      <h3 style={{
        fontFamily: FONT.display,
        fontSize: TYPE.xl,
        fontWeight: WEIGHT.semibold,
        color: T.ink,
        margin: 0,
        lineHeight: 1.2,
      }}>
        {title}
      </h3>
      <p style={{
        fontSize: TYPE.base,
        color: T.inkMuted,
        margin: 0,
        maxWidth: 360,
        lineHeight: 1.5,
      }}>
        {description}
      </p>
      {cta && (
        <button onClick={cta.onClick} /* ... estilo do Button primary */>
          {cta.icon && <cta.icon size={14} />}
          {cta.label}
        </button>
      )}
      {link && (
        <a href={link.href} style={{ fontSize: TYPE.sm, color: T.inkMuted }}>
          {link.label}
        </a>
      )}
    </div>
  );
}

Regras

Faça

  • Use ícone semanticamente significativo (Inbox para mensagens, FileSearch para busca, Lock para permissão).
  • Título no imperativo: "Crie sua primeira área", não "Lista vazia".
  • Descrição em 1-2 frases. Diga o que fazer, não o problema.
  • CTA sempre que houver ação imediata possível.
  • Padding generoso (SP[16] vertical) — empty state precisa respirar.
  • Animação sutil de mount (fade + slide).

Não faça

  • Não use cor carmim ou ícone de erro (não é erro).
  • Não use ilustrações grandes (custo perceptual alto).
  • Não use 2 CTAs primários competindo.
  • Não chame de "vazio" — chame de "primeiro acesso", "sem resultados", etc.
  • Não esconda navegação ou outras ações da página inteira.
  • Não force descrição longa — 2 frases máx.

Tokens usados

TokenValorPapel
T.surface#FFFFFFbackground do container
T.border#E3DFD2borda dashed
T.brassTintrgba(164,124,43,0.08)background do círculo do ícone
T.brass#A47C2Bcor do ícone
TYPE.xl21fontSize do título
TYPE.base13fontSize da descrição
SP[16]64padding vertical do container
RADIUS.lg18borderRadius