APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

React と ChakraUI で動くローディング画面を作る

はじめに

こんにちは!ACS 事業部の田中です。

皆さん、アプリケーションのローディング画面は好きでしょうか?
私はそんなに好きではありません。
しかし悲しいことに、待ち時間が絶対に存在しないアプリケーションは皆無でしょう。

ならば、ローディング画面は少しでも見ていて楽しいものであってほしい!
ということで、今回は React と ChakraUI を使ってアニメーションがついたローディング画面を作ってみようと思います。

こいつ、動くぞ…!

つくるもの

楽しいローディング画面の定義は人それぞれでしょうが、私はうごうご動いているものが好きです。
今回はシンプルに、画面中央でくるくると線が回るローディング画面を作ります。

UI のスタイリングには ChakraUI を使用します。
ChakraUI は公式のインストール手順に従い、pnpm add のほかアプリのルートを ChakraUI の Provider でラップします。 chakra-ui.com

React でアニメーションやモーションを付ける際は FramerMotion ライブラリの利用などが選択肢に上がると思いますが、今回実装するものはシンプルなアニメーションなため、疑似要素で実装します。 chakra-ui.com

本記事の動作バージョンは以下です。

node: v20.4.0
typescript: 5.2.2
react: 18.2.0
chakra-ui/react: 2.8.1

完成品

上記のローディング画面は以下のように実装しています。

import { Box, Text, keyframes } from "@chakra-ui/react";

const rotate1 = keyframes`
  0%   {transform:translate(-50%, -50%) rotate(0)}
  100% {transform:translate(-50%, -50%) rotate(360deg)}
`;

const rotate2 = keyframes`
  0%   {transform:translate(-50%, -50%) rotate(0)}
  100% {transform:translate(-50%, -50%) rotate(-360deg)}
`;

const blurText = keyframes`
  0% {filter: blur(0px);}
  100% {filter: blur(4px);}
`;

export const CycleComponent = () => {
  return (
    <Box h="100vh" w="100vw" bg="#222">
      <Box
        pos="absolute"
        top="50%"
        left="50%"
        transform="translate(-51%, -50%)"
        w="240px"
        h="240px"
        borderWidth="5px"
        borderColor="#701B1B"
        borderRadius="120px"
        boxSizing="border-box"
        _after={{
          content: '""',
          position: "absolute",
          top: "50%",
          left: "50%",
          transform: "translate(-50%, -100%)",
          w: "100px",
          h: "200%",
          bg: "#222",
          animation: `${rotate2} 30s infinite linear`,
        }}
      >
        <Box
          pos="absolute"
          top="50%"
          left="50%"
          transform="translate(-50%, -50%)"
          w="200px"
          h="200px"
          color="#888"
          textAlign="center"
          borderWidth="5px"
          borderColor="#803030"
          borderRadius="100px"
          zIndex="20"
          textTransform="uppercase"
          _before={{
            content: '""',
            position: "absolute",
            top: "50%",
            left: "50%",
            transform: "translate(-53%, -50%)",
            w: "106%",
            h: "100px",
            bg: "#222",
            animation: `${rotate2} 10s infinite linear`,
          }}
          _after={{
            content: '""',
            position: "absolute",
            top: "50%",
            left: "50%",
            transform: "translate(-50%, -53%)",
            w: "100px",
            h: "106%",
            bg: "#222",
            animation: `${rotate1} 15s infinite linear`,
          }}
        >
          <Box
            pos="absolute"
            top="50%"
            left="50%"
            transform="translate(-50%, -50%)"
            w="140px"
            h="140px"
            borderWidth="5px"
            borderColor="#A04949"
            borderRadius="70px"
            zIndex="20"
            display="flex"
            justifyContent="center"
            alignItems="center"
            _after={{
              content: '""',
              position: "absolute",
              top: "50%",
              left: "50%",
              transform: "translate(-50%, -55%)",
              w: "40px",
              h: "110%",
              bg: "#222",
              animation: `${rotate1} 8s infinite linear`,
            }}
          >
            {["L", "o", "a", "d", "i", "n", "g"].map((letter, index) => (
              <Text
                as="span"
                key={index}
                fontSize="sm"
                fontWeight="bold"
                mx="0.5"
                color="#B36D6D"
                zIndex="100"
                animation={`${blurText} 1.5s ${index / 5}s infinite alternate`}
              >
                {letter}
              </Text>
            ))}
          </Box>
        </Box>
      </Box>
    </Box>
  );
};

肝になるのはアニメーション定義の部分です。 このコードでは、3 つのアニメーションを定義しています。
keyframesによりアニメーションの中間地点のスタイルを定義することによって、一連のアニメーションの中間ステップを制御することができます。

rotate1, rotate2

rotate1 ,rotate2 はそれぞれ線の回転用アニメーションを定義しています。
開始時と終了時の回転度合いを定義しており、rotate1 は時計回り、rotate2 は反時計回りに回転します。

const rotate1 = keyframes`
  0%   {transform:translate(-50%, -50%) rotate(0)}
  100% {transform:translate(-50%, -50%) rotate(360deg)}
`;
const rotate2 = keyframes`
  0%   {transform:translate(-50%, -50%) rotate(0)}
  100% {transform:translate(-50%, -50%) rotate(-360deg)}
`;

blurText

blurText は文字のぼやけアニメーションを定義しています。
開始時にはぼかしなし、終了時には 4px のぼかしをかけるようにしています。

const blurText = keyframes`
  0% {filter: blur(0px);}
  100% {filter: blur(4px);}
`;

アニメーションの定義ができたら、あとはそれを適用する要素に animation プロパティを指定するだけです。

このローディングアニメーションでは、複数の Box 要素に疑似要素を使用して追加の回転アニメーションを実装しています。
外側と内側の Box では _after により半分覆う形状を作り回転させることで動きを出しています。
真ん中の Box では _before_afterを使用して横方向と縦方向の半分をそれぞれ覆う形状を作り、回転させることで動きを出しています。

おわりに

このように、アニメーション用のライブラリを利用せずとも単純な動きのアニメーションであれば簡単に実装できてしまいます。
なんとなくアニメーションというと複雑なコードを書かないといけないんじゃないかという先入観があったのですが、考え方はとてもシンプルですね。

動きが入るだけでぐっとアプリケーションが "それっぽく" もなりますし、ローディング画面の退屈さもまぎれることでしょう。
延々見続けてもいいくらいですね。

え?ローディング画面じゃなくて肝心のコンテンツを早く表示してほしい?
そりゃそうだ。


私達 ACS 事業部は Azure・AKS を活用した内製化のご支援をしております。ご相談等ありましたらぜひご連絡ください。

www.ap-com.co.jp

また、一緒に働いていただける仲間も募集中です!
切磋琢磨しながらスキルを向上できる、エンジニアには良い環境だと思います。ご興味を持っていただけたら嬉しく思います。

www.ap-com.co.jp

本記事の投稿者: 田中 絢子
AKSをメインにしたインフラとアプリの領域をご支援することが多いです。