DEV Community

Cover image for Creating shimmering text, color change text in Loader using Framer Motion, Next.js 13
AnnaSidiakina
AnnaSidiakina

Posted on

Creating shimmering text, color change text in Loader using Framer Motion, Next.js 13

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
Enter fullscreen mode Exit fullscreen mode

Run the development server

npm run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the src directory, create a libfolder 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the same app folder create a file loading.js:

const Loading = () => {
  return <div>Loading...</div>;
};

export default Loading;
Enter fullscreen mode Exit fullscreen mode

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".

Menu
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} />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Now we have our movies:

Movies
Let's add some styling. Well that looks much better!

Styled movies

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.

Start figma
I used the following colors:

  • 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.

Transform text to svg
Now do this for each letter. Don't forget to change their color to yellow. You have to get something like this:

Create yellow letters
We need yellow letters separately to animate them individually.
Next step is to place yellow letters above blue ones that way:

Place yellow letters above blue
Last step is we need to group blue svg text and finally group all yellow letters with blue text.

Group
Now export 3 files (two ellipses and group with letters) from Figma to the public/images folder in the project.
Create the Loaderfolder 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;
`;
Enter fullscreen mode Exit fullscreen mode

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);
  }
`;
Enter fullscreen mode Exit fullscreen mode

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;
  }
`;
Enter fullscreen mode Exit fullscreen mode

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:

Letters svg component
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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now the loader looks like this: ellipses are rotating, letters are static.

Static letters
Install Framer Motion

npm i framer-motion
Enter fullscreen mode Exit fullscreen mode

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"
      />

Enter fullscreen mode Exit fullscreen mode

Create a delay function:

const delay = (i) => {
  return 0 + i * 0.2;
};
Enter fullscreen mode Exit fullscreen mode

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 animateand transitionproperties 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"
      />
Enter fullscreen mode Exit fullscreen mode

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)