KDS
Feedback

Skeleton

Bloco placeholder pulsante que reserva geometria enquanto o conteúdo real carrega.

Overview

Skeleton é o componente de loading mais subestimado do KDS. Visualmente é apenas um retângulo pulsante (animate-pulse em cima de bg-accent), mas o trabalho real é arquitetural: ele reserva a geometria do conteúdo real para que, quando os dados chegarem, nada se mova. Isso é o coração da prevenção de [[ui/layout/cumulative-layout-shift]].

A regra principal: a forma do skeleton deve casar com a forma do conteúdo que vai aparecer. Se o conteúdo final é um avatar circular de 40px ao lado de duas linhas de texto, o skeleton precisa ser exatamente isso — não um retângulo gigante de placeholder.

Preview
Três geometrias lado a lado — texto, avatar, card.

Anatomy

<Skeleton
  ├─ [data-slot="skeleton"]
  ├─ animate-pulse
  ├─ rounded-md (override para -full / -lg)
  └─ bg-accent
/>

Skeleton é apenas um <div>. Toda a geometria — width, height, rounded-* — é controlada pelo consumidor via className. Isso é intencional: cada caso de uso tem geometria diferente.

Usage

import { Skeleton } from "@kalvner/kds/feedback/skeleton";

export function UserCardLoading() {
  return (
    <div
      className="flex items-center gap-3 p-3"
      aria-busy="true"
      aria-live="polite"
    >
      <Skeleton className="size-10 rounded-full" />
      <div className="flex-1 space-y-2">
        <Skeleton className="h-3 w-[60%]" />
        <Skeleton className="h-3 w-[40%]" />
      </div>
    </div>
  );
}

Props

PropTipoDefaultDescrição
classNamestringGeometria (width, height, rounded). É a única coisa que você costuma passar.
...restReact.ComponentProps<'div'>Tudo o que <div> aceita (id, aria-*).

Sem props específicas — Skeleton é puramente um <div> estilizado. A geometria fica em className.

Variants

Text line
Linha de texto. h-4 para texto base.
Multi-line
Width descendente imita parágrafo real.
Avatar circle
size-10 + rounded-full = 40px circular.
Card block
Bloco grande — banner, imagem, mídia.
Button placeholder
h-9 casa com a altura padrão de Button.
Tag pill
h-5 + rounded-full pra Badge / chips.

States

Pulsing (default)
animate-pulse comunica "estou trabalhando".
Reduced motion
Em SO com motion reduzido, fica estático.

O Tailwind v4 desabilita animate-pulse automaticamente para usuários com prefers-reduced-motion: reduce.

Composition

List loading
Três itens — avatar + duas linhas.
Profile card loading
Banner + avatar sobreposto + meta.

When to use

  • Sempre que o consumidor precisa esperar dados que vão ocupar uma forma previsível — lista de mensagens, card de perfil, linha de tabela.
  • Quando substituir um spinner faria a UI parecer mais lenta. Skeleton comunica progresso sem capturar atenção.
  • Em primeira renderização de páginas com SSR — o servidor entrega o shell + skeletons; o cliente hidrata e troca por dados reais.

When not to use

  • Para botões durante submit — use disabled + spinner no próprio botão. Skeleton ali parece troca de elemento.
  • Para conteúdo cuja geometria você não conhece (long-form, rich-text). Aí o skeleton vira chute — prefira spinner ou progressive loading.
  • Para erros — Skeleton sozinho infinito faz o usuário acreditar que ainda está carregando. Tenha um timeout que troca para Alert de erro com retry.
  • Para ações instantâneas (< 200ms). O flicker do skeleton é pior que o vazio passageiro.

Best practices

  • Geometria do skeleton = geometria do conteúdo. Reserve a mesma altura, a mesma largura aproximada e o mesmo raio. Sem isso, há layout shift no momento da troca — exatamente o que o skeleton deveria evitar. Ver [[ui/layout/cumulative-layout-shift]].
  • Use aria-busy="true" no container. O Skeleton em si não tem semântica para AT — quem comunica o estado de loading é a região ao redor. Combine com aria-live="polite" para anunciar quando o conteúdo carregar.
  • Pulsar é melhor que rodar. Spinner quando você não tem geometria; Skeleton quando você tem. A regra é a forma — não a duração.
  • Não anime cor para cinza-mais-escuro. O animate-pulse do Tailwind faz opacidade. Trocar por animação de gradiente ("shimmer") é uma escolha legítima, mas exige garantir reduced-motion.
  • Não exiba mais skeletons do que o número provável de itens. Se a lista típica tem 5 itens, mostre 5 — não 20. Skeletons em excesso parecem fake content.

Accessibility

ConcernComportamento
RolesSem role específico — Skeleton é puramente visual. A semântica de "carregando" deve vir do container via aria-busy="true".
Live regionCombine com aria-live="polite" no container para anunciar quando o conteúdo aparecer.
KeyboardNão recebe foco.
Reduced motionanimate-pulse é desligado automaticamente para usuários com prefers-reduced-motion: reduce. O bloco fica estático e a comunicação visual continua via bg-accent em contraste com o fundo.
Contrastebg-accent tem contraste sutil — não dependa do skeleton para transmitir informação além de "espaço reservado".
Touch / mobileNão interativo. Mantenha a geometria do skeleton casando com o conteúdo real para evitar shift quando o usuário rola.
  • Card — frequente container de skeletons em loading de dashboards e perfis.
  • Avatar — pareie um Skeleton circular do mesmo tamanho enquanto carrega.
  • [[ui/layout/cumulative-layout-shift]] — fundamentação completa sobre por que reservar geometria importa.

On this page