While building my last app, I was fascinated by the Loader with changing color company name that our designers provided. It is really calming to look at it and wait in meditation until the page loads 😄.
Install Next.js
$ npm create next-app@latest
Need to install the following packages:
create-next-app@13.4.16
Ok to proceed? (y) y
√ What is your project named? ... animated-loader
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like to use `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to customize the default import alias? ... No / Yes
Run the development server
npm run dev
Clean up all unnecessary code, we don't need it anymore.
For this project, I'll use Styled Components. Don't forget to add "use client" at the top of each styled.js
file.
Let's create our basic styled navbar component first and adding it to our global layout file in the project. If you're not familiar with what a layout file is - it's basically a new file type introduced in Next.js v13 which can be used to create the layout for your site. By default, Next.js would create a layout file for you in the root of your app named layout.js
. We can import there header with menu and footer.
npm i styled-components
In the src
directory, create a lib
folder and add a registry.js
file inside with the following code:
"use client";
import React, { useState } from "react";
import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
export default function StyledComponentsRegistry({ children }) {
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement();
styledComponentsStyleSheet.instance.clearTag();
return <>{styles}</>;
});
if (typeof window !== "undefined") return <>{children}</>;
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
);
}
Now wrap our app with the StyledComponentsRegistry
. Navigate to the layout.js
file in the app
folder and add it:
import "./globals.css";
import { Inter } from "next/font/google";
import { Suspense } from "react";
import Loading from "./loading";
import { Menu } from "@/components/navigation/Navigation";
import StyledComponentsRegistry from "@/lib/registry";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "Animated loader",
description: "animated loader",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<div className="main">
<div className="gradient" />
</div>
<StyledComponentsRegistry>
<main className="app">
<Menu />
<Suspense fallback={<Loading />}>{children}</Suspense>
</main>
</StyledComponentsRegistry>
</body>
</html>
);
}
In the same app
folder create a file loading.js
:
const Loading = () => {
return <div>Loading...</div>;
};
export default Loading;
We'll pass this to the fallback
of the Suspense
component as our future animated loader.
I've also created two pages: "About" and "Movies".
Let's fetch movies from https://developer.themoviedb.org/docs. You'll need to create account and get API key. I usually keep that kind of stuff in .env
file.
Next, in the app
directory, create two folders: about
and movies
, and create a page.js
file in each folder for the respective pages.
Add the following code to movies/page.js:
import Movies from "@/components/Movies/Movies";
export async function getMovies() {
let res = await fetch(
`https://api.themoviedb.org/3/trending/movie/day?api_key=${process.env.NEXT_PUBLIC_TMDB_API}`
);
await new Promise((resolve) => setTimeout(resolve, 3000));
return res.json();
}
export default async function MoviesPage() {
const { results } = await getMovies();
return <Movies movies={results} />;
}
In src
directory create the components
folder. Inside it, create another folder named Movies
. In this folder, create Movies.jsx
and Movies.styled.js
files.
In Movies.jsx
, create a functional component:
import { Section } from "../Section/Section";
import Image from "next/image";
export default function Movies({ movies }) {
console.log("movies", movies);
return (
<Section>
<h1>Movies</h1>
<ul>
{movies.map((movie) => (
<li key={movie.id}>
<Image
src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`}
alt={movie.title}
width={200}
height={500}
/>
<p>{movie.title}</p>
</li>
))}
</ul>
</Section>
);
}
You'll need to modify next.config.js
to display images:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["image.tmdb.org"],
remotePatterns: [
{
protocol: "https",
hostname: "**.tmdb.org",
port: "",
pathname: "/t/p/w200**",
},
],
},
};
module.exports = nextConfig;
Now we have our movies:
Let's add some styling. Well that looks much better!
Now, let's move on to creating our loader. To achieve that, we need to do a little magic in Figma. Create two ellipses with a size around 300px and add the company name text inside them.
- Blue: #8DCBE6
- Green: #9DF1DF
- Yellow (for contrast-changing color): #FFEA20
The text is blue, size 40 and font "Jua" extra bold. Right-click and choose "Outline Stroke".
Now we need to create our yellow letters. For that I created every letter separately. Then right click and choose again "Ouline stroke". That way we create svg letters one by one.
Now do this for each letter. Don't forget to change their color to yellow. You have to get something like this:
We need yellow letters separately to animate them individually.
Next step is to place yellow letters above blue ones that way:
Last step is we need to group blue svg text and finally group all yellow letters with blue text.
Now export 3 files (two ellipses and group with letters) from Figma to the public/images
folder in the project.
Create the Loader
folder in components
. Create the file Loader.jsx
. It will be our common loader component.
First we need to wrap the loader in container. Create Loader.styled.js
:
export const Container = styled.div`
position: relative;
height: 80vh;
width: 100%;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
`;
Now add Loader Container
with ellipses. Now we create ellipse animation. I add ellipses as background image
of ::before
and ::after
of LoaderContainer
. One ellipse rotates clock wise, another in opposite direction. Import keyframes
from styled-components
.
const rotate = keyframes`
0% {
transform: rotate(1deg) scale(1);
}
40% {
transform: rotate(180deg) scale(0.8);
}
100% {
transform: rotate(360deg) scale(1);
}
`;
const rotateOpposite = keyframes`
0% {
transform: rotate(1deg) scale(1);
}
40% {
transform: rotate(-180deg) scale(0.8);
}
100% {
transform: rotate(-360deg) scale(1);
}
`;
Add the animation to ellipses:
export const LoaderContainer = styled.div`
position: absolute;
transform: translate(-50%, -50%);
left: 50%;
top: 50%;
height: 300px;
width: 300px;
margin: 0 auto;
&::before {
content: " ";
display: block;
position: absolute;
height: 300px;
width: 300px;
background-image: url(/images/blueEllipse.svg);
background-repeat: no-repeat;
background-position: center;
animation: ${rotate} 4s linear infinite;
}
&::after {
content: " ";
display: block;
position: absolute;
height: 300px;
width: 300px;
background-image: url(/images/greenEllipse.svg);
background-repeat: no-repeat;
background-position: center;
animation: ${rotateOpposite} 4s linear infinite;
}
`;
Import Container
and LoaderContainer
to Loader.jsx
. From letters.svg
in images create component. You can use SVGR or do it manually. It will look like this:
Import Letters
to Loader
component:
"use client";
import { Letters } from "./Letters";
import { Container, LoaderContainer } from "./Loader.styled";
export const Loader = () => {
return (
<Container>
<LoaderContainer />
<Letters />
</Container>
);
};
Now the loader looks like this: ellipses are rotating, letters are static.
npm i framer-motion
Import {motion}
from framer-motion
in Letters.jsx
. Now we need to isolate all yellow letters. Add id
attributes to identify each letter.
Replace <svg>
to <motion.svg>
and <path>
to <motion.path>
(only for yellow letter)
<motion.path
id="m" //for letter M
d="M25.36 25.64C25 //.... rest"
fill="#FFEA20"
/>
Create a delay
function:
const delay = (i) => {
return 0 + i * 0.2;
};
Pass i
in each motion.path
tag that way that each letter starts the animation a bit later than the previous one. Set the duration to 1 second. Add animate
and transition
properties to each motion.path
, using the delay
function for the delay of each letter (e.g., delay(0)
for the first letter and delay(9)
for the last):
<motion.path
id="m"
animate={{ opacity: [0, 1, 0] }}
transition={{
duration: duration,
ease: "linear",
repeat: Infinity,
delay: delay(0),
}}
d="M25.36 25.64C25.3333 ... etc"
fill="#FFEA20"
/>
<motion.path
id="o1"
animate={{ opacity: [0, 1, 0] }}
transition={{
duration: duration,
ease: "linear",
repeat: Infinity,
delay: delay(1),
}}
d="M33.1513 11.0982C34.0313 ...etc"
fill="#FFEA20"
/>
<motion.path
id="v"
animate={{ opacity: [0, 1, 0] }}
transition={{
duration: duration,
ease: "linear",
repeat: Infinity,
delay: delay(2),
}}
d="M66.77 11.5714C66.8767 ...etc"
fill="#FFEA20"
/>
Import the Loader component to loading.js in the app folder and check how it works.
You can choose any colors you like, adjust the duration, increase the delay, and experiment with different animations. The possibilities are endless, and it's all up to your imagination and creativity!
Top comments (0)