Return home

loading circles

A fun loading indicator using 8 circles

> COMPONENTS:
> motion.div
> useAnimate
> spring
> KEYWORDS:
loadingloadertranslatecirclerotatespinner
> SOURCE CODE:

"use client"

import { AnimationSequence, DOMSegmentWithTransition, motion, useAnimate } from "motion/react"
import { useEffect } from "react"

const NUMBER_OF_CIRCLES = 8

const LoadingCircles = () => {
  const [scope, animate] = useAnimate()

  useEffect(() => {
    const radius = 100 
    const timeline: AnimationSequence = [
      ...Array.from(Array(NUMBER_OF_CIRCLES)).map((circle, index) => {
        // Calculate the angle in radians, starting at -90° (12 o'clock)
        const angle = ((-90 + (360 / NUMBER_OF_CIRCLES) * index) * Math.PI) / 180
        const x = radius * Math.cos(angle)
        const y = radius * Math.sin(angle)
        const transition: DOMSegmentWithTransition = [".circle-"+index, { x, y }, { duration: 0.2 }]
        return transition
      }),
      ['.circle-wrapper', { rotate: 360 }, { duration: 2.2, type: 'spring' }],
      ...Array.from(Array(NUMBER_OF_CIRCLES)).map((circle, index) => {
        const transition: DOMSegmentWithTransition = [".circle-"+index, { x: 0, y: 0 }, { duration: 0.2 }]
        return transition
      }),
    ]

    animate(timeline, { repeat: Infinity })
  }, [scope, animate])

  return (
    <div className="w-40 h-40" ref={scope}>
      <motion.div className="w-full h-full flex items-center justify-center relative circle-wrapper">
        <div className="w-10 h-10 rounded-full bg-amber-500 absolute" />
        {Array.from(Array(NUMBER_OF_CIRCLES)).map((circle, index) => (
          <motion.div
            className={"w-10 h-10 rounded-full bg-amber-500 absolute circle-"+index}
            key={index}
          />
        ))}
      </motion.div>
    </div>
  )
}

export default LoadingCircles