Back to all notes

Skeletons and Placeholders: Show Structure While Content Loads

Improve perceived performance with skeleton UIs that match the eventual layout.

2 min read

Skeletons and placeholders show the rough shape of content while it loads. Instead of a blank screen or a spinner, the user sees where the text and images will appear.

A spinner tells the user nothing about what’s coming. A skeleton that matches the final layout keeps them oriented, prevents the content from jumping in, and makes the wait feel shorter than a centered “Loading…” ever does.

The Blank Spinner

The version every app ships first:

import { useState } from "react";

function Profile({ user, loading }) {
  if (loading) return <p>Loading...</p>;
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState(null);

  // Simulate loading
  setTimeout(() => {
    setUser({ name: "John Doe", email: "john@example.com" });
    setLoading(false);
  }, 2000);

  return (
    <div>
      <h1>Skeleton & Placeholder Pattern</h1>
      <Profile user={user} loading={loading} />
    </div>
  );
}

export default App;

A Skeleton That Matches the Layout

A ProfileSkeleton with gray blocks sized like the real content, rendered until the data lands:

import { useState, useEffect } from "react";

function ProfileSkeleton() {
  return (
    <div>
      <div style={{ height: "24px", width: "200px", background: "#e0e0e0", marginBottom: "8px", borderRadius: "4px" }}></div>
      <div style={{ height: "16px", width: "300px", background: "#e0e0e0", borderRadius: "4px" }}></div>
    </div>
  );
}

function Profile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Simulate loading
    const timer = setTimeout(() => {
      setUser({ name: "John Doe", email: "john@example.com" });
      setLoading(false);
    }, 2000);
    return () => clearTimeout(timer);
  }, []);

  return (
    <div>
      <h1>Skeleton & Placeholder Pattern</h1>
      {loading ? <ProfileSkeleton /> : <Profile user={user} />}
    </div>
  );
}

export default App;

The rule that matters: the skeleton’s shape should match the final layout, block for block, so nothing shifts when the swap happens. Keep the skeleton itself lightweight, CSS animations rather than JavaScript, and when data streams in piece by piece, replace the skeletons incrementally instead of all at once.