code in my day
Published on

How to Use an Animated Skeleton Loading Screen in Next.js

5 min read
Authors

Summary

Everyone wants fast and optimized websites with minimal wait times. Loaders and spinners are commonly used to inform users that data is being fetched and will soon appear on the screen. In modern applications, skeleton screens have replaced traditional loaders, as they give the impression that data is loading quickly, and users get a sneak peek of how the final screen will look. Skeleton screens are particularly useful when server responses take some time. Many popular websites, such as YouTube, LinkedIn, and Spotify, have already adopted this approach.

In this tutorial, we won't reinvent the wheel by creating skeleton screens from scratch with CSS. Instead, we'll use the popular UI library Shadcn to display skeleton screens when dealing with slow API responses.

Prerequisites

Before getting started, make sure you have the following:

  • Node Latest Version
  • Editor (Visual Studio Code)
  • Basic understanding of reactjs and hooks

Setting Up a Next.js and Tailwind CSS App

Create a Project

To create a Next.js project with Tailwind CSS, TypeScript, and ESLint, run the following npm command:

npx create-next-app@latest nextjs-tailwind-app --typescript --tailwind --eslint

Follow the installation process, answer the questions, and choose to use the src/ directory without the "App Router" option.

install-project

Run the CLI

Navigate to the project directory nextjs-tailwind-app and install Shadcn UI by running:

cd nextjs-tailwind-app
npx shadcn-ui@latest init

Configure components.json

components.json

Open the src/pages/index.tsx file in Visual Studio Code or your preferred code editor and remove everything inside the <main></main> tags. We'll use a free API called JsonPlaceholder for this demonstration.

Below is the code that will fetch a list of users using the JsonPlaceholder fake API:

const getUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users')
  if (!res.ok) throw new Error(res.statusText)
  await new Promise(resolve => setTimeout(resolve, 3000));
  const users = await res.json()
  return users
}

We will utilize the React hooks useState and useEffect to store the response from getUsers() in a state variable called users.

const [users, setUsers] = useState<User[]>([])  
useEffect(() => {
getUsers().then((users) => setUsers(users))

}, [])

We can display the list of users using the following JSX syntax. With the help of Tailwind CSS, we'll arrange 10 users in three rows, each with a four-column layout.

    <main className="p-10 grid sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-4 gap-5">
        {users.map((user) => (
          <div key={user.id} className="grid-flow-col auto-cols-max gap-12">
            <div>
            <Image
              src={`https://avatars.dicebear.com/api/avataaars/${user.name}.svg`}
              alt={user.name}
              width={96}
              height={96}
              className="object-cover rounded-md"
            />
            </div>
            <h2 className="text-sm font-bold p-6 mb-3 bg-slate-100">{user.name}</h2>
            <div className="text-gray-500 pb-8 mb-4 bg-slate-100">{user.email}</div>
            <div className='bg-slate-100 p-4'>{user.address.street}, {user.address.suite}, {user.address.city} - {user.address.zipcode}</div>
          </div>
        ))}
    </main>

Running the Project

To start the project, run:

npm run dev

You should now see 10 users on the screen, each with their profile image, name, email, and address.

Introducing the Skeleton Screen

Since the response from our fake API is expected to be quick, we'll need to simulate a slow API response by adding a delay to the getUsers() function.

await new Promise(resolve => setTimeout(resolve, 3000));

Upon refreshing the screen, you'll notice a blank (white) screen for three seconds before the fetched users are displayed.

Implementing the Skeleton Screen

Installing the Skeleton Component To install the Skeleton component, run the following command:

npx shadcn-ui@latest add skeleton

This will create a nested directory structure inside the src directory and place the skeleton component file inside src/components/ui/skeleton.tsx.

Next, create a new file src/pages/loading.tsx inside the pages directory.

Copy and paste the following JSX code into the loading.tsx file:

import { Skeleton } from "@/components/ui/skeleton"

export default function Loading() {
    return (
        <main className="p-10 grid sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-4 gap-5">
            {Array.from({length: 12},(_, i) => i + 1).map((id) => (
                <div key={id} className="grid-flow-col auto-cols-max gap-12">
                    <Skeleton className="object-none w-32 h-32 rounded-full custom-position bg-gray-200" />
                    <Skeleton className="h-10 w-full p-6 mb-4 bg-slate-100" />
                    <Skeleton className="h-10 w-full p-8 mb-4 bg-slate-100" />
                    <Skeleton className='h-10 w-full p-10 mb-4 bg-slate-100' />
                </div>
                )
            )}
        </main>
    )
}

The final step is to display the loading screens before rendering the user list:

return (
    users.length <= 0 ? <Loading /> : (
        <main ...
        </main>
    )
)

Your index.tsx file should now look like this:

import { useState, useEffect } from 'react'
import Image from 'next/image'
import Loading from './loading'

type User = {
  id: number
  name: string
  email: string,
  address: {  
    street: string,
    suite: string,
    city: string,
    zipcode: string,
  }
}

const getUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users')
  if (!res.ok) throw new Error(res.statusText)
  await new Promise(resolve => setTimeout(resolve, 3000));
  const users = await res.json()
  return users
}

export default function Home() {
  const [users, setUsers] = useState<User[]>([])

  useEffect(() => {
    getUsers().then((users) => setUsers(users))
    
  }, [])

  return (
    users.length <= 0 ? <Loading /> : (
    <main className="p-10 grid sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-4 gap-5">
        {users.map((user) => (
          <div key={user.id} className="grid-flow-col auto-cols-max gap-12">
            <div>
            <Image
              src={`https://avatars.dicebear.com/api/avataaars/${user.name}.svg`}
              alt={user.name}
              width={96}
              height={96}
              className="object-cover rounded-md"
            />
            </div>
            <h2 className="text-sm font-bold p-6 mb-3 bg-slate-100">{user.name}</h2>
            <div className="text-gray-500 pb-8 mb-4 bg-slate-100">{user.email}</div>
            <div className='bg-slate-100 p-4'>{user.address.street}, {user.address.suite}, {user.address.city} - {user.address.zipcode}</div>
          </div>
        ))}
    </main>
    )
  )
}

Testing Your App

Refresh the browser, and you'll see the skeleton screens displayed before the user data loads.

skeleton_screen

This implementation improves user experience by providing visual feedback during loading times.