Logo Spinner – Animated Logo with Circular Progress

PreviousNext

A beautifully animated React component that displays a spinning logo inside a customizable circular progress ring. A versatile and stylish loading spinner featuring a central animated logo, a customizable progress ring, and optional percentage display.

Docs
react-marketcomponent

Preview

Loading preview…
logo-spinner.tsx
"use client"

import { useState, useEffect } from "react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"

export interface LogoSpinnerProps {
  /**
   * The URL of the logo to display
   * @default "https://www.react-market.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Freact-market-logo.50bbe1d5.png&w=256&q=75"
   */
  logoSrc?: string
  /**
   * The size of the spinner in pixels
   * @default 100
   */
  size?: number
  /**
   * The color of the progress ring
   * @default "#3b82f6"
   */
  color?: string
  /**
   * The background color of the progress ring
   * @default "#e5e7eb"
   */
  backgroundColor?: string
  /**
   * The thickness of the progress ring in pixels
   * @default 4
   */
  thickness?: number
  /**
   * The duration of the animation in seconds
   * @default 2
   */
  duration?: number
  /**
   * Whether to show the percentage text
   * @default true
   */
  showPercentage?: boolean
  /**
   * The current progress value (0-100)
   * If not provided, the component will animate from 0 to 100 automatically
   */
  progress?: number
  /**
   * Additional class names to apply to the container
   */
  className?: string
}

export function LogoSpinner({
  logoSrc = "https://www.react-market.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Freact-market-logo.50bbe1d5.png&w=256&q=75",
  size = 100,
  color = "#3b82f6",
  backgroundColor = "#e5e7eb",
  thickness = 4,
  duration = 2,
  showPercentage = true,
  progress: externalProgress,
  className,
}: LogoSpinnerProps) {
  const [progress, setProgress] = useState(0)

  // If external progress is provided, use it
  // Otherwise, animate from 0 to 100
  useEffect(() => {
    if (externalProgress !== undefined) {
      setProgress(externalProgress)
      return
    }

    const interval = setInterval(() => {
      setProgress((prev) => {
        if (prev >= 100) {
          clearInterval(interval)
          return 100
        }
        return prev + 1
      })
    }, duration * 10)

    return () => clearInterval(interval)
  }, [externalProgress, duration])

  // Calculate the circumference of the circle
  const radius = size / 2 - thickness
  const circumference = 2 * Math.PI * radius

  // Calculate the stroke-dashoffset based on the progress
  const strokeDashoffset = circumference - (progress / 100) * circumference

  return (
    <div
      className={cn("relative flex items-center justify-center", className)}
      style={{ width: size, height: size }}
      role="progressbar"
      aria-valuenow={progress}
      aria-valuemin={0}
      aria-valuemax={100}
    >
      {/* Background circle */}
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="absolute">
        <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke={backgroundColor} strokeWidth={thickness} />
      </svg>

      {/* Progress circle */}
      <svg
        width={size}
        height={size}
        viewBox={`0 0 ${size} ${size}`}
        className="absolute"
        style={{ transform: "rotate(-90deg)" }}
      >
        <motion.circle
          cx={size / 2}
          cy={size / 2}
          r={radius}
          fill="none"
          stroke={color}
          strokeWidth={thickness}
          strokeDasharray={circumference}
          strokeDashoffset={strokeDashoffset}
          strokeLinecap="round"
          initial={{ strokeDashoffset: circumference }}
          animate={{ strokeDashoffset }}
          transition={{ duration: 0.5, ease: "easeInOut" }}
        />
      </svg>

      {/* Logo */}
      <motion.div
        className="absolute"
        style={{
          width: size * 0.6,
          height: size * 0.6,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
        animate={{
          rotate: externalProgress === undefined ? [0, 360] : 0,
        }}
        transition={{
          repeat: externalProgress === undefined ? Number.POSITIVE_INFINITY : 0,
          duration: 3,
          ease: "linear",
        }}
      >
        <img src={logoSrc || "/placeholder.svg"} alt="Loading logo" className="w-full h-full object-contain" />
      </motion.div>

      {/* Percentage text */}
      {showPercentage && (
        <div className="absolute bottom-0 w-full text-center font-medium text-sm" style={{ color }}>
          <motion.span
            key={progress}
            initial={{ opacity: 0, y: 5 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.2 }}
          >
            {Math.round(progress)}%
          </motion.span>
        </div>
      )}
    </div>
  )
}

Installation

npx shadcn@latest add @react-market/logo-spinner

Usage

import { LogoSpinner } from "@/components/logo-spinner"
<LogoSpinner />