Skeleton
Placeholder animado com o formato do conteúdo que ainda está carregando.
'use client';
import { Skeleton } from '@/components/ui/skeleton';
export default function SkeletonDemo() {
return (
<div className="flex items-center gap-4">
<Skeleton className="size-12 rounded-full" />
<div className="grid gap-2">
<Skeleton className="h-4 w-[220px]" />
<Skeleton className="h-4 w-[160px]" />
</div>
</div>
);
}Instalação
npx shadcn@latest add @lenstech/skeletonUso
import { Skeleton } from "@/components/ui/skeleton"<Skeleton className="h-4 w-48" />Defina tamanho e forma com classes utilitárias (h-*, w-*, rounded-full…) para imitar o conteúdo final.
Acessibilidade
O Skeleton é decorativo (aria-hidden). Marque o contêiner que está carregando com aria-busy="true" e, se
necessário, um aria-label como "Carregando clientes", para que leitores de tela saibam o que está acontecendo.
Exemplos
Card
'use client';
import { Skeleton } from '@/components/ui/skeleton';
export default function SkeletonCard() {
return (
<div className="grid w-full max-w-xs gap-3 rounded-xl border p-4">
<Skeleton className="aspect-video w-full rounded-lg" />
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
<div className="mt-2 flex items-center justify-between">
<Skeleton className="h-6 w-20" />
<Skeleton className="h-9 w-28" />
</div>
</div>
);
}Lista
'use client';
import { Skeleton } from '@/components/ui/skeleton';
export default function SkeletonList() {
return (
<ul className="grid w-full max-w-md divide-y rounded-lg border" aria-busy="true" aria-label="Carregando clientes">
{Array.from({ length: 4 }, (_, i) => (
<li key={i} className="flex items-center justify-between gap-4 px-4 py-3">
<div className="grid gap-1.5">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="h-5 w-16 rounded-md" />
</li>
))}
</ul>
);
}Trocando pelo conteúdo
Renderize o Skeleton enquanto os dados carregam e substitua pelo conteúdo real, mantendo as mesmas dimensões para a página não "pular".
'use client';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
export default function SkeletonLoading() {
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!loading) return;
const timer = setTimeout(() => setLoading(false), 1800);
return () => clearTimeout(timer);
}, [loading]);
return (
<div className="flex w-full max-w-sm flex-col items-center gap-4">
<div className="w-full rounded-xl border p-4" aria-busy={loading}>
{loading ? (
<div className="grid gap-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-8 w-40" />
<Skeleton className="h-3 w-24" />
</div>
) : (
<div className="grid gap-1">
<p className="text-sm text-muted-foreground">Faturamento do mês</p>
<p className="font-mono text-2xl font-semibold tabular-nums">R$ 48.230,00</p>
<p className="text-xs text-success">+12% vs. agosto</p>
</div>
)}
</div>
<Button variant="outline" size="sm" isDisabled={loading} onPress={() => setLoading(true)}>
Recarregar
</Button>
</div>
);
}Quando usar
- Use no carregamento inicial de listas, cards e painéis cujo formato já é conhecido.
- Não use para ações do usuário (salvar, enviar) — use
isPendingnoButtonou umSpinner. - Não use para tarefas longas com progresso mensurável — use
Progress.
API
Skeleton
Aceita todas as props de um <div>. Não há props próprias: o formato vem de className.
Prop
Type