Pare de escrever skeletons na mão
Heads up — this one's in Portuguese. Your browser's translate button will get you most of the way.

Você provavelmente já escreveu esse trecho de código em algum momento da sua carreira:
<div className="h-4 w-32 animate-pulse rounded bg-gray-200" />
E depois mais uns dez parecidos, empilhados até o formato lembrar vagamente a UI que foi desenhada para estar ali. Funciona no dia em que foi escrito. O problema aparece depois: quando alguém adiciona uma linha a mais ao card, muda o padding, troca o avatar de quadrado pra redondo. O skeleton quase nunca acompanha essas modificações: ele fica lá esquecido, não quebra teste, não gera erro no console, não aparece em code review. Até virar aquele piscar estranho de layout que todo mundo percebe e ninguém prioriza 🤦♂️
Hoje eu vim mostrar uma solução "milagrosa" para esse problema. O boneyard resolve isso de uma maneira bem simples e bem intuitiva: em vez de você escrever o skeleton à mão, ele o extrai do seu componente real.
O que vamos fazer
Instalar a biblioteca, envolver um componente e rodar um comando. O uso final fica assim:
import { Skeleton } from "boneyard-js/react";
function ProductPage() {
const { data, isLoading } = useProduct();
return (
<Skeleton name="product-card" loading={isLoading}>
{data && <ProductCard data={data} />}
</Skeleton>
);
}
O que eu mais gosto do boneyard é que você não precisa escrever nenhuma medida na mão. O name é a chave que liga esse ponto da UI ao arquivo de bones gerado.
Como funciona: você marca, o CLI mede, o app importa
São três passos, e vale entender como cada um funciona.
Marcação é o wrap acima. O <Skeleton> marca no componente os pontos que devem virar skeleton.
Medição é o CLI:
npm install boneyard-js
npx boneyard-js build
Com o dev server já rodando, o CLI abre um browser headless, visita a aplicação, encontra cada <Skeleton name="..."> e mede os elementos filhos nos breakpoints padrão: 375, 768 e 1280. Se o componente depende de dados de uma API ou está protegido por autenticação, você passa um fixture, um mock com a mesma estrutura, só que com um conteúdo estático.
Importação é uma linha só, no root da aplicação (no Next.js, por exemplo, o layout.tsx):
import "./bones/registry";
Isso popula um map interno e, a partir daí, todo <Skeleton name="..."> resolve seus bones sozinho.
O que tem dentro do .bones.json
Essa é a parte que costuma ficar ali escondida e na qual você normalmente não toca, mas é bom entender como funciona, e é mais simples do que parece.
Cada skeleton vira um arquivo dentro de bones/, no nosso caso, product-card.bones.json com o nome usado no <Skeleton>, o viewport em que a medição aconteceu, as dimensões do container e a lista de bones:
{
"name": "product-card",
"viewportWidth": 375,
"width": 288,
"height": 310,
"bones": [
[0, 0, 100, 310, 12, true],
[5.9, 17, 88.2, 144, 8],
[5.9, 181, 59, 16, 4]
]
}
Cada bone é um array: [x, y, w, h, r, isContainer?]. O x e o w vêm em porcentagem da largura do container, o y e o h em pixels, o r é o border radius, e o último elemento, quando está presente, marca aquele bone como container. Um bone com a flag container = true representa um elemento de container e ele é renderizado com um tom mais claro para que os filhos se destaquem.
É isso. Não tem layout engine em runtime, não tem cópia do seu componente: o runtime cria um container position: relative, joga cada bone visível como um filho position: absolute e injeta os keyframes da animação. Por isso o bundle é pequeno e por isso não existe layout shift: as medidas vieram do seu próprio DOM.
A demo abaixo renderiza um card com bones escritos exatamente nesse formato. Clique para alternar entre o skeleton e o conteúdo real:
loading=trueRepare que as bordas não "pulam" e o skeleton acompanha perfeitamente o tamanho do card.
O fluxo completo
// app/layout.tsx
import "./bones/registry";
// components/ProductPage.tsx
import { Skeleton } from "boneyard-js/react";
export function ProductPage() {
const { data, isLoading } = useProduct();
return (
<Skeleton
name="product-card"
loading={isLoading}
animate="shimmer"
transition={300}
fixture={
<ProductCard
data={{
title: "Produto exemplo",
description: "Lorem ipsum",
price: 100,
}}
/>
}
>
{data && <ProductCard data={data} />}
</Skeleton>
);
}
npx boneyard-js build --watch
O --watch é o modo de hot-reload, então o skeleton acompanha as atualizações do componente enquanto você desenvolve.
Por que vale a pena entender isso
O ganho não é economizar várias divs. É que o skeleton deixa de ser código gerado a mão e necessário manutenção para um gerado por comando e versionado no git.
Eu não sei se essa biblioteca é a melhor solução para uma aplicação grande em produção. Para MVPs e SaaS pequenos resolve, bem a parte chata.
