Skeleton Loading with Suspense in Next.js 13

My strategy for handling skeleton loading with Suspense.

Published 29 December 2022 - Updated 29 December 2022 - by Alvar Lagerlöf

With Next.js 13 released recently, I've started thinking a lot more about how loading happens and is communicated to the user. I ended up with something like this:

Website alvar.dev, projects page. A grid of projects are loading, indicated by a skeleton UI representing 4 loading projects.



Suddenly, with Suspense, it is way more scaleable and convenient to do something pretty yet simple. However, in my adventures, I ran into some gotchas along the way and found some strategies to keep it all tidy. Let's dive in!

Hold up, Suspense?

No worries. Suspense is a special new component that handles something loading. While that is happening, it renders a fallback. Let's look at a minimal example:


function BlogPosts() {
  return (
    <section>
      <h2>Blog posts</h2>
      
      <Suspense fallback={<p>Loading...</p>}>
        <Posts />
      </Suspense>
      
    </section>
  )
}

This example is not complete, but we can start to see a structure. While <Posts /> are loading, show <p>Loading...</p>.

Let's take a look at <Posts />. There are two ways to do it, depending on if it's rendered on the server or the client.

// If rendered on a server
async function Posts() {
  const posts = await fetch("http://example.com");
  
  return (
    <ul className="space-y-6">
      {posts.map(({title, description}) => {
        return (
          <article className="space-y-2">
            <h3>{title}</h3>
            <p>{description}</p>
          </article>
        )
      })}
    </ul>
  )
}

// If rendered on a client
function Posts() {
  const posts = use(fetch("http://example.com"))
  
  return (
    <ul className="space-y-6">
      {posts.map({title, description} => {
        return (
          <article className="space-y-2">
            <h3>{title}</h3>
            <p>{description}</p>
          </article>
        )
      })}
    </ul>
  )
}

These both are very similar, and also behave similarly in terms of Suspense. There are big differences underneath, but we don't need to consider them for our skeletons.

Let's make some Skeletons!

I use Tailwind, and there are lots of useful utilities to make making skeletons simpler. Even so, I found making a <Skeleton /> component to be useful. Mine looks something like this:

export default function Skeleton({ className }: { className: string }) {
  return <div className={`bg-slate-200 motion-safe:animate-pulse rounded ${className}`} />;
}

You get a few nice things from doing this:

  • The skeleton has a slight pulsating animation, to indicate that there is activity happening.
  • The animation does not show if the user prefers reduced motion (via motion-safe).
  • There's a default design with slightly rounded corners and a background color.

Now let's take this, and try to make a component indicating loading for our <Posts />.

function Loading() {
  return (
    <div className="space-y-6">
      <div className="space-y-2">
        <Skeleton className="w-[30ch] h-[1.25rem]"/>
        <Skeleton className="w-[45ch] h-[1rem]"/>
      </div>
      <div className="space-y-2">
        <Skeleton className="w-[30ch] h-[1.25rem]"/>
        <Skeleton className="w-[45ch] h-[1rem]"/>
      </div>
      <div className="space-y-2">
        <Skeleton className="w-[30ch] h-[1.25rem]"/>
        <Skeleton className="w-[45ch] h-[1rem]"/>
      </div>
    </div
  )
}

This gives us something like this:

Skeleton UI showing three items loading. Each has a title and a description.

We can now use this component in our <Suspense> like this.

function BlogPosts() {
  return (
    <section>
      <h2>Blog posts</h2>
      
      <Suspense fallback={<Loading/>}>
        <Posts />
      </Suspense>
      
    </section>
  )
}

That works! We can stop here, but there is an opportunity for a different structure.

What if each component provides their own skeleton

What if each Post in our <Posts /> list provided its own Skeleton? Let's take a look at how that plays out:

// Post.tsx

import Skeleton from "../components/Skeleton"

export function Post({title, description}) {
  return (
    <article className="space-y-2">
      <h3>{title}</h3>
      <p>{description}</p>
    </article>
  )
}

export function PostLoading() {
  return (
    <div className="space-y-2">
      <Skeleton className="w-[30ch] h-[1.25rem]"/>
      <Skeleton className="w-[45ch] h-[1rem]"/>
    </div>
  )
}

// RecentPosts.tsx

import { Post, PostLoading } from "./Post"

function RecentPosts() {
  return (
    <section>
      <h2>Blog posts</h2>
      
      <ul className="space-y-6">
        <Suspense fallback={<PostsLoading/>}>
          <Posts />
        </Suspense>
      </ul>
      
    </section>
  )
}

async function Posts() {
  const posts = await fetch("http://example.com");
  
  return (
    <>
      {posts.map((post) => {
        return <Post {...post} />
      })}
    </>
  )
}

function PostsLoading() {
  return (
    <>
      <PostLoading />
      <PostLoading />
      <PostLoading />
    </>
  )
}

This is pretty much what I landed in. How much to separate the components into different files is up to you, but the idea of a component exporting both its complete and loading state was a powerful idea for composing skeleton UIs on my site.

If you've tried it yourself, I'd love to hear your take on skeleton UIs with Suspense.