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.
"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>
)
}npx shadcn@latest add @react-market/logo-spinnerimport { LogoSpinner } from "@/components/logo-spinner"<LogoSpinner />