Loading skeletons that do not lie

A skeleton is a placeholder UI shaped like the real content. Done well, it tells the user the page is about to render something specific. Done poorly, it is worse than a spinner because it implies a layout the real content does not match and causes layout shift when it resolves.

Three rules for skeletons that work. Match the final dimensions so there is no layout shift. Use the same number of placeholder items you expect to render. Keep them simple and fast to render, because they are what users see the most when the network is slow.

app/books/BookListSkeleton.tsx
tsx
export function BookListSkeleton() {
  return (
    <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="rounded-lg border border-stone-200 p-4">
          <div className="h-5 w-3/4 animate-pulse rounded bg-stone-200" />
          <div className="mt-2 h-4 w-1/2 animate-pulse rounded bg-stone-200" />
          <div className="mt-4 h-10 w-full animate-pulse rounded bg-stone-200" />
        </div>
      ))}
    </div>
  );
}

A skeleton that matches the real card layout.

Quiz: Quiz

Loading practice…