shadcnloaders
uiloadersource: ok · refreshed
Loader
A single flexible loading spinner component with 42 built-in animated variants (spinner, dots, bars, skeleton, DNA, matrix, atom, solar system, radar sweep and more), Tailwind dark mode support, and configurable size.
npx shadcn add "https://shadcn-loaders.vercel.app/r/loader.json"free
Visual style
dark-futuristic
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files1 · source cached 10/8/2026, 6:18:45 AM
registry/loader.tsx
import * as React from "react";
export type LoaderVariant =
| "classic-spinner"
| "dots-pulse"
| "pulse-ring"
| "bars"
| "square-spin"
| "double-bounce"
| "circular-progress"
| "wave-loader"
| "skeleton"
| "glow-pulse"
| "clock"
| "hourglass"
| "gear"
| "orbit"
| "snake"
| "infinity"
| "text-shimmer"
| "grid"
| "heartbeat"
| "floating-bubble"
| "matrix"
| "loader-dna"
| "hex-spin"
| "concentric-rings"
| "dots-rotate"
| "morphing-hex"
| "ripple-pulse"
| "orbiting-spheres"
| "paired-revolution"
| "eyes-gaze"
| "ripple-wave"
| "audio-wave"
| "fading-ring"
| "morphing-shape"
| "dots-chase"
| "atom"
| "solar-system"
| "radar-sweep"
| "blocks-wave"
| "coffee-cup"
| "progress-slider"
| "flower-spin"
| "bouncing-ball"
| "walking-steps"
| "corner-chase";
export interface LoaderProps {
variant: LoaderVariant;
size?: "sm" | "md" | "lg" | "xl";
className?: string;
}
const sizeClasses = {
sm: "w-4 h-4",
md: "w-8 h-8",
lg: "w-12 h-12",
xl: "w-16 h-16",
};
const loaderSnakeKeyframes = `
@keyframes loader-snake {
0%, 100% { clip-path: inset(0 0 95% 0); }
25% { clip-path: inset(0 95% 0 0); }
50% { clip-path: inset(95% 0 0 0); }
75% { clip-path: inset(0 0 0 95%); }
}
.animate-loader-snake { animation: loader-snake 2s linear infinite; }
`;
const loaderShimmerKeyframes = `
@keyframes loader-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.animate-loader-shimmer { animation: loader-shimmer 2s infinite; }
`;
const loaderPingKeyframes = `
@keyframes loader-ping {
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
`;
const loaderMorphHexKeyframes = `
@keyframes loader-morph-hex {
0%, 100% { transform: rotate(0deg) scale(1); }
50% { transform: rotate(180deg) scale(0.875); }
}
`;
const variantKeyframes: Partial<Record<LoaderVariant, string>> = {
snake: loaderSnakeKeyframes,
skeleton: loaderShimmerKeyframes,
infinity: loaderShimmerKeyframes,
"text-shimmer": loaderShimmerKeyframes,
matrix: loaderShimmerKeyframes,
"ripple-pulse": loaderPingKeyframes,
"morphing-hex": loaderMorphHexKeyframes,
};
export const Loader: React.FC<LoaderProps> = ({
variant,
size = "md",
className = "",
}) => {
const containerSize = sizeClasses[size] ?? sizeClasses.md;
const primaryColorClass = "text-zinc-900 dark:text-zinc-50";
const primaryBgClass = "bg-zinc-900 dark:bg-zinc-50";
const primaryBorderClass = "border-zinc-900 dark:border-zinc-50";
const keyframes = variantKeyframes[variant];
const hourglassBorder = size === "sm" ? 8 : size === "md" ? 12 : size === "lg" ? 16 : 20;
let content: React.ReactNode = null;
switch (variant) {
case "classic-spinner":
content = (
<div
className={`${containerSize} border-2 border-zinc-200 dark:border-zinc-800 border-t-zinc-900 dark:border-t-zinc-50 rounded-full animate-spin ${className}`}
/>
);
break;
case "dots-pulse":
content = (
<div className={`flex gap-1 items-center ${className}`}>
{[0, 1, 2].map((i) => (
<div
key={i}
className={`${size === "sm" ? "w-1 h-1" : "w-2 h-2"} ${primaryBgClass} rounded-full animate-bounce`}
style={{ animationDelay: `${i * 0.15}s` }}
/>
))}
</div>
);
break;
case "pulse-ring":
content = (
<div className={`relative ${containerSize} ${className}`}>
<div className="absolute inset-0 border-2 border-zinc-900 dark:border-zinc-50 rounded-full animate-ping opacity-75" />
<div className="absolute inset-2 border-2 border-zinc-600 dark:border-zinc-400 rounded-full animate-pulse" />
</div>
);
break;
case "bars":
content = (
<div className={`flex items-end gap-1 h-8 ${className}`}>
{[0, 1, 2, 3, 4].map((i) => (
<div
key={i}
className={`w-1 ${primaryBgClass} rounded-full animate-[pulse_1s_ease-in-out_infinite]`}
style={{
height: `${20 + (i * 15) % 80}%`,
animationDelay: `${i * 0.1}s`,
}}
/>
))}
</div>
);
break;
case "square-spin":
content = (
<div
className={`${containerSize} ${primaryBgClass} animate-[spin_2s_linear_infinite] rounded-sm shadow-sm ${className}`}
/>
);
break;
case "double-bounce":
content = (
<div className={`relative ${containerSize} ${className}`}>
<div
className={`absolute inset-0 ${primaryBgClass} rounded-full opacity-60 animate-bounce`}
/>
<div className="absolute inset-0 bg-zinc-400 dark:bg-zinc-600 rounded-full opacity-60 animate-bounce [animation-delay:700ms]" />
</div>
);
break;
case "circular-progress":
content = (
<svg
className={`${containerSize} animate-spin ${className}`}
viewBox="0 0 50 50"
>
<circle
className="stroke-zinc-900 dark:stroke-zinc-50 fill-none"
cx="25"
cy="25"
r="20"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray="80, 200"
/>
</svg>
);
break;
case "wave-loader":
content = (
<div className={`flex gap-0.5 ${className}`}>
{[...Array(10)].map((_, i) => (
<div
key={i}
className={`w-1 h-4 ${primaryBgClass} animate-[pulse_1.5s_infinite]`}
style={{ animationDelay: `${i * 0.1}s` }}
/>
))}
</div>
);
break;
case "skeleton":
content = (
<div
className={`relative overflow-hidden bg-zinc-100 dark:bg-zinc-900 rounded border border-zinc-200/50 dark:border-zinc-800/50 ${containerSize} ${className}`}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-zinc-200 dark:via-zinc-800 to-transparent animate-loader-shimmer -translate-x-full" />
</div>
);
break;
case "glow-pulse":
content = (
<div
className={`${primaryBgClass} rounded-full shadow-[0_0_20px_rgba(0,0,0,0.2)] dark:shadow-[0_0_20px_rgba(255,255,255,0.1)] animate-pulse ${containerSize} ${className}`}
/>
);
break;
case "clock":
content = (
<div
className={`${containerSize} border-2 border-zinc-300 dark:border-zinc-700 rounded-full relative ${className}`}
>
<div className="absolute top-1/2 left-1/2 w-[40%] h-0.5 bg-zinc-900 dark:bg-zinc-50 origin-left -rotate-90 animate-[spin_2s_linear_infinite]" />
<div className="absolute top-1/2 left-1/2 w-[30%] h-0.5 bg-zinc-500 origin-left -rotate-90 animate-[spin_8s_linear_infinite]" />
</div>
);
break;
case "hourglass":
content = (
<div
className={`${containerSize} relative animate-[spin_2s_ease-in-out_infinite] ${className}`}
>
<div
className="absolute inset-0 border-t-zinc-900 dark:border-t-zinc-50 border-x-transparent border-b-transparent rounded-t-full"
style={{
borderTopWidth: hourglassBorder,
borderLeftWidth: hourglassBorder,
borderRightWidth: hourglassBorder,
borderBottomWidth: hourglassBorder,
}}
/>
<div
className="absolute inset-0 border-b-zinc-400 dark:border-b-zinc-600 border-x-transparent border-t-transparent rounded-b-full"
style={{
borderTopWidth: hourglassBorder,
borderLeftWidth: hourglassBorder,
borderRightWidth: hourglassBorder,
borderBottomWidth: hourglassBorder,
}}
/>
</div>
);
break;
case "gear":
content = (
<div
className={`${containerSize} ${primaryColorClass} animate-[spin_3s_linear_infinite] ${className}`}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-full h-full"
>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2 2 2 0 0 1-2 2 2 2 0 0 0-2 2 2 2 0 0 1-2 2 2 2 0 0 0-2 2v.44a2 2 0 0 0 2 2 2 2 0 0 1 2 2 2 2 0 0 0 2 2 2 2 0 0 1 2 2 2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2 2 2 0 0 1 2-2 2 2 0 0 0 2-2 2 2 0 0 1 2-2 2 2 0 0 0 2-2v-.44a2 2 0 0 0-2-2 2 2 0 0 1-2-2 2 2 0 0 0-2-2 2 2 0 0 1-2-2 2 2 0 0 0-2-2Z" />
<circle cx="12" cy="12" r="3" />
</svg>
</div>
);
break;
case "orbit":
content = (
<div
className={`${containerSize} relative flex items-center justify-center ${className}`}
>
<div className={`w-2 h-2 ${primaryBgClass} rounded-full`} />
<div className="absolute w-full h-full border border-zinc-200 dark:border-zinc-800 rounded-full animate-spin">
<div
className={`absolute top-0 left-1/2 -translate-x-1/2 w-1.5 h-1.5 ${primaryBgClass} rounded-full`}
/>
</div>
</div>
);
break;
case "snake":
content = (
<div
className={`${containerSize} border-2 border-zinc-100 dark:border-zinc-900 relative rounded-md overflow-hidden ${className}`}
>
<div
className={`absolute inset-0 border-2 ${primaryBorderClass} animate-loader-snake`}
/>
</div>
);
break;
case "infinity":
content = (
<div className={`${containerSize} ${primaryColorClass} ${className}`}>
<svg viewBox="0 0 100 50" className="w-full h-full">
<path
fill="none"
stroke="currentColor"
strokeWidth="4"
strokeDasharray="10 5"
className="animate-[loader-shimmer_2s_linear_infinite]"
d="M25,25 C25,10 40,10 50,25 C60,40 75,40 75,25 C75,10 60,10 50,25 C40,40 25,40 25,25"
/>
</svg>
</div>
);
break;
case "text-shimmer":
content = (
<div
className={`text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-zinc-400 via-zinc-900 dark:via-zinc-100 to-zinc-400 bg-[length:200%_auto] animate-[loader-shimmer_2s_linear_infinite] ${className}`}
>
Loading...
</div>
);
break;
case "grid":
content = (
<div className={`grid grid-cols-3 gap-1 ${className}`}>
{[...Array(9)].map((_, i) => (
<div
key={i}
className={`w-2 h-2 ${primaryBgClass} rounded-sm animate-pulse`}
style={{ animationDelay: `${i * 0.1}s` }}
/>
))}
</div>
);
break;
case "heartbeat":
content = (
<div
className={`${containerSize} ${primaryColorClass} animate-[ping_1.5s_infinite] flex items-center justify-center ${className}`}
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
className="w-full h-full"
>
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg>
</div>
);
break;
case "matrix":
content = (
<div
className={`flex flex-col gap-0.5 overflow-hidden ${containerSize} items-center ${className}`}
>
{[...Array(4)].map((_, i) => (
<div
key={i}
className="w-full h-1 bg-zinc-100 dark:bg-zinc-900 relative"
>
<div
className={`absolute top-0 bottom-0 w-4 ${primaryBgClass} shadow-[0_0_10px_rgba(0,0,0,0.1)]`}
style={{
left: "-20%",
animation: `loader-shimmer 2s linear infinite`,
animationDelay: `${i * 0.5}s`,
}}
/>
</div>
))}
</div>
);
break;
case "loader-dna":
content = (
<div
className={`flex justify-center items-center h-full gap-1 ${className}`}
>
{[...Array(5)].map((_, i) => (
<div key={i} className="flex flex-col gap-2">
<div
className={`w-2 h-2 ${primaryBgClass} rounded-full animate-bounce`}
style={{ animationDelay: `${i * 0.1}s` }}
/>
<div
className="w-2 h-2 bg-zinc-300 dark:bg-zinc-700 rounded-full animate-bounce"
style={{ animationDelay: `${i * 0.1 + 0.5}s` }}
/>
</div>
))}
</div>
);
break;
case "floating-bubble":
content = (
<div
className={`relative overflow-hidden ${containerSize} border border-zinc-200 dark:border-zinc-800 rounded-lg bg-zinc-50/50 dark:bg-zinc-900/50 ${className}`}
>
{[...Array(5)].map((_, i) => {
const width = [6, 10, 8, 5, 11][i];
const left = [15, 35, 55, 75, 45][i];
const duration = [2.5, 3.8, 2.1, 4.5, 3.2][i];
const delay = [0.2, 1.1, 0.5, 1.8, 0.8][i];
return (
<div
key={i}
className={`absolute ${primaryBgClass} opacity-30 rounded-full animate-bounce`}
style={{
width: `${width}px`,
height: `${width}px`,
left: `${left}%`,
bottom: "-20%",
animationDuration: `${duration}s`,
animationDelay: `${delay}s`,
}}
/>
);
})}
</div>
);
break;
case "hex-spin":
content = (
<div
className={`${containerSize} ${primaryColorClass} flex items-center justify-center ${className}`}
>
<svg
viewBox="0 0 100 100"
className="w-full h-full animate-[spin_1.6s_linear_infinite]"
fill="none"
stroke="currentColor"
strokeWidth="4"
>
<polygon
points="50,10 80,30 80,70 50,90 20,70 20,30"
className="stroke-current"
/>
</svg>
</div>
);
break;
case "concentric-rings":
content = (
<div
className={`relative ${containerSize} flex items-center justify-center ${className}`}
>
<div className="absolute inset-0 rounded-full border-2 border-zinc-200 dark:border-zinc-800 animate-ping" />
<div className="absolute inset-2 rounded-full border-2 border-zinc-300 dark:border-zinc-700 animate-ping [animation-delay:200ms]" />
<div className="absolute inset-4 rounded-full border-2 border-zinc-400 dark:border-zinc-600 animate-pulse [animation-delay:400ms]" />
</div>
);
break;
case "dots-rotate":
content = (
<div className={`relative ${containerSize} ${className}`}>
<div className="absolute inset-0 flex items-center justify-center animate-[spin_1.2s_linear_infinite]">
{[0, 1, 2].map((i) => (
<div
key={i}
className="absolute w-2 h-2 bg-zinc-900 dark:bg-zinc-50 rounded-full"
style={{ transform: `rotate(${i * 120}deg) translateY(-40%)` }}
/>
))}
</div>
</div>
);
break;
case "morphing-hex":
content = (
<div className={`${containerSize} ${className}`}>
<svg
viewBox="0 0 100 100"
className="w-full h-full"
fill="none"
stroke="currentColor"
strokeWidth="4"
>
<polygon
points="50,10 80,30 80,70 50,90 20,70 20,30"
className="stroke-zinc-900 dark:stroke-zinc-50"
style={{
transformBox: "fill-box",
transformOrigin: "center",
animation: "loader-morph-hex 3s ease-in-out infinite",
}}
/>
</svg>
</div>
);
break;
case "ripple-pulse":
content = (
<div
className={`relative ${containerSize} flex items-center justify-center overflow-hidden ${className}`}
>
<div className={`absolute w-2 h-2 ${primaryBgClass} rounded-full`} />
{[0, 1, 2].map((i) => (
<div
key={i}
className="absolute rounded-full border-2 border-zinc-900 dark:border-zinc-50"
style={{
width: `${12 + i * 8}px`,
height: `${12 + i * 8}px`,
animation: `loader-ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite`,
animationDelay: `${i * 0.4}s`,
opacity: 1 - i * 0.3,
}}
/>
))}
</div>
);
break;
case "orbiting-spheres":
content = (
<div className={`${containerSize} ${className}`}>
<style>{`
@keyframes loader-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes loader-orbit-motion {
0% { transform: translateX(8px) scale(0.73); opacity: 0.65; }
25% { transform: translateX(0%) scale(0.47); opacity: 0.3; }
50% { transform: translateX(-8px) scale(0.73); opacity: 0.65; }
75% { transform: translateX(0%) scale(1); opacity: 1; }
100% { transform: translateX(8px) scale(0.73); opacity: 0.65; }
}
`}</style>
<div
className="relative flex items-center justify-center w-full h-full"
style={{ animation: "loader-rotate 2.5s linear infinite" }}
>
{[0, 1].map((i) => (
<div
key={i}
className={`absolute rounded-full ${primaryBgClass}`}
style={{
width: "35%",
height: "35%",
animation: `loader-orbit-motion 1.5s linear ${i * -0.75}s infinite`,
}}
/>
))}
</div>
</div>
);
break;
case "paired-revolution":
content = (
<div
className={`${containerSize} relative flex items-center justify-center ${className}`}
>
<style>{`
@keyframes loader-twin-orbit-rotate {
100% {
transform: rotate(360deg) translate(155%);
}
}
`}</style>
<div
className={`absolute rounded-full ${primaryBgClass}`}
style={{
width: "10px",
height: "10px",
zIndex: 10,
}}
/>
<div className="absolute w-full h-full">
{[0, 1].map((i) => (
<div
key={i}
className={`absolute rounded-full ${primaryBgClass}`}
style={{
width: "10px",
height: "10px",
top: "50%",
left: "50%",
transform: "rotate(0deg) translate(155%)",
animation: "loader-twin-orbit-rotate 1.4s ease infinite",
animationDelay: `${i * 0.7}s`,
marginTop: "-5px",
marginLeft: "-5px",
}}
/>
))}
</div>
</div>
);
break;
case "eyes-gaze":
content = (
<div
className={`relative flex items-center justify-center gap-4 ${className}`}
>
<style>{`
@keyframes loader-gaze {
0% { transform: translateX(-6px); }
25% { transform: translateX(-6px); }
50% { transform: translateX(6px); }
75% { transform: translateX(6px); }
100% { transform: translateX(-6px); }
}
`}</style>
{[0, 1].map((i) => (
<div
key={i}
className="relative w-8 h-8 bg-zinc-900 dark:bg-zinc-50 rounded-full flex items-center justify-center"
>
<div
className="w-3 h-3 bg-white dark:bg-zinc-900 rounded-full flex items-center justify-center"
style={{ animation: "loader-gaze 2s ease-in-out infinite" }}
>
<div className="w-2 h-2 bg-zinc-900 dark:bg-zinc-50 rounded-full" />
</div>
</div>
))}
</div>
);
break;
case "ripple-wave":
content = (
<div
className={`relative ${containerSize} flex items-center justify-center ${className}`}
>
<div
className={`absolute inset-0 border-4 ${primaryBorderClass} rounded-full animate-ping opacity-75`}
/>
<div className={`w-3 h-3 ${primaryBgClass} rounded-full`} />
</div>
);
break;
case "audio-wave":
content = (
<div className={`flex items-center gap-1 h-6 ${className}`}>
{[0, 1, 2, 3].map((i) => (
<div
key={i}
className={`w-1 h-full ${primaryBgClass} rounded-full animate-[pulse_1s_ease-in-out_infinite]`}
style={{ animationDelay: `${i * 0.2}s` }}
/>
))}
</div>
);
break;
case "fading-ring":
content = (
<svg
className={`${containerSize} animate-spin ${className}`}
viewBox="0 0 32 32"
>
<circle
className="stroke-zinc-900 dark:stroke-zinc-50 fill-none opacity-25"
cx="16"
cy="16"
r="14"
strokeWidth="3"
/>
<circle
className="stroke-zinc-900 dark:stroke-zinc-50 fill-none"
cx="16"
cy="16"
r="14"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray="20 60"
/>
</svg>
);
break;
case "morphing-shape":
content = (
<div className={`${containerSize} ${className}`}>
<style>{`
@keyframes loader-morph-shape {
0%, 100% { border-radius: 4px; transform: rotate(0deg); }
50% { border-radius: 50%; transform: rotate(180deg); }
}
`}</style>
<div
className={`w-full h-full ${primaryBgClass}`}
style={{ animation: "loader-morph-shape 2s ease-in-out infinite" }}
/>
</div>
);
break;
case "dots-chase":
content = (
<div className={`flex items-center justify-center gap-2 ${className}`}>
{[0, 1, 2].map((i) => (
<div
key={i}
className={`w-2 h-2 ${primaryBgClass} rounded-full animate-[pulse_1.2s_infinite]`}
style={{ animationDelay: `${i * 0.2}s` }}
/>
))}
</div>
);
break;
case "atom":
content = (
<div
className={`relative ${containerSize} flex items-center justify-center ${className}`}
>
<style>{`
@keyframes loader-atom-orbit {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
<div className={`absolute z-10 w-1/5 h-1/5 rounded-full ${primaryBgClass}`} />
{[0, 60, 120].map((rot, i) => (
<div
key={rot}
className="absolute inset-0"
style={{
transform: `rotate(${rot}deg)`,
animation: `loader-atom-orbit ${2 + i * 0.5}s linear infinite`,
animationDelay: `${i * -0.8}s`,
}}
>
<div className="absolute top-1/2 left-1/2 w-full h-2/5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-zinc-900/40 dark:border-zinc-50/40" />
<div
className={`absolute top-1/2 left-0 w-1/5 h-1/5 -translate-y-1/2 -translate-x-1/2 rounded-full ${primaryBgClass}`}
/>
</div>
))}
</div>
);
break;
case "solar-system":
content = (
<div
className={`relative ${containerSize} flex items-center justify-center ${className}`}
>
<style>{`
@keyframes loader-solar-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
<div className={`absolute z-10 w-[16%] h-[16%] rounded-full ${primaryBgClass}`} />
{[
{ inset: "8%", speed: 3.5, delay: 0 },
{ inset: "20%", speed: 2.6, delay: -1.3 },
{ inset: "32%", speed: 5.4, delay: -0.4 },
].map((ring, i) => (
<div
key={i}
className="absolute rounded-full border border-zinc-900/30 dark:border-zinc-50/30"
style={{
inset: ring.inset,
animation: `loader-solar-spin ${ring.speed}s linear infinite`,
animationDelay: `${ring.delay}s`,
}}
>
<div
className={`absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rounded-full ${
i === 1 ? "bg-zinc-400 dark:bg-zinc-600" : primaryBgClass
}`}
style={{
width: `${[9, 6, 7][i]}%`,
height: `${[9, 6, 7][i]}%`,
}}
/>
</div>
))}
</div>
);
break;
case "radar-sweep":
content = (
<div
className={`relative overflow-hidden rounded-full border border-zinc-900/30 dark:border-zinc-50/30 ${containerSize} ${primaryColorClass} ${className}`}
>
<style>{`
@keyframes loader-radar-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
<div
className="absolute rounded-full border border-zinc-900/20 dark:border-zinc-50/20"
style={{ inset: "22%" }}
/>
<div
className="absolute rounded-full border border-zinc-900/15 dark:border-zinc-50/15"
style={{ inset: "42%" }}
/>
<div className="absolute top-1/2 left-0 right-0 h-px bg-zinc-900/15 dark:bg-zinc-50/15" />
<div className="absolute top-0 bottom-0 left-1/2 w-px bg-zinc-900/15 dark:bg-zinc-50/15" />
<div
className="absolute inset-0 rounded-full"
style={{
background:
"conic-gradient(from 0deg, currentColor 0deg, transparent 62deg)",
animation: "loader-radar-spin 1.8s linear infinite",
opacity: 0.5,
}}
/>
<div
className={`absolute w-[12%] h-[12%] rounded-full ${primaryBgClass} animate-ping`}
style={{ left: "68%", top: "58%", opacity: 0.9 }}
/>
<div
className={`absolute w-[8%] h-[8%] rounded-full ${primaryBgClass} animate-ping`}
style={{ left: "26%", top: "30%", opacity: 0.7, animationDelay: "0.6s" }}
/>
</div>
);
break;
case "blocks-wave":
content = (
<div className={`flex items-end gap-1 h-8 ${className}`}>
<style>{`
@keyframes loader-wave-bob {
0%, 100% { transform: scaleY(0.35); }
50% { transform: scaleY(1); }
}
`}</style>
{[0, 1, 2, 3, 4].map((i) => (
<div
key={i}
className={`w-3 ${primaryBgClass} rounded-sm`}
style={{
height: `${[34, 65, 100, 65, 34][i]}%`,
transformOrigin: "bottom",
animation: `loader-wave-bob ${1.1}s ease-in-out infinite`,
animationDelay: `${i * 0.12}s`,
}}
/>
))}
</div>
);
break;
case "coffee-cup":
content = (
<div
className={`relative flex items-center justify-center ${containerSize} ${primaryColorClass} ${className}`}
>
<style>{`
@keyframes loader-steam {
0% { transform: translateY(0) translateX(0) scaleX(1); opacity: 0; }
30% { opacity: 0.9; }
100% { transform: translateY(-200%) translateX(45%) scaleX(1.4); opacity: 0; }
}
`}</style>
<div
className="absolute flex gap-[10%]"
style={{ bottom: "56%", width: "42%", height: "16%" }}
>
{[0, 1, 2].map((i) => (
<div
key={i}
className="w-[16%] h-full rounded-full bg-current"
style={{
animation: `loader-steam ${1.6 + i * 0.3}s ease-in-out infinite`,
animationDelay: `${i * 0.45}s`,
marginTop: i === 1 ? "-24%" : 0,
}}
/>
))}
</div>
<div className="relative" style={{ width: "58%", height: "40%" }}>
<div
className="absolute bottom-0 rounded-b-md border-[3px] border-current"
style={{ left: "6%", width: "70%", height: "100%", borderTop: "none" }}
/>
<div
className="absolute bottom-[12%] rounded-r-full border-[3px] border-current"
style={{ right: "0%", width: "24%", height: "26%" }}
/>
<div
className="absolute bottom-0 bg-current opacity-35"
style={{ left: "6%", width: "70%", height: "16%", borderRadius: "0 0 5px 5px" }}
/>
</div>
</div>
);
break;
case "progress-slider":
content = (
<div
className={`relative w-24 h-2 overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-800 ${className}`}
>
<style>{`
@keyframes loader-slide {
0% { transform: translateX(-110%); }
100% { transform: translateX(260%); }
}
`}</style>
<div
className={`absolute top-0 bottom-0 w-1/3 rounded-full ${primaryBgClass}`}
style={{ animation: "loader-slide 1.4s ease-in-out infinite" }}
/>
</div>
);
break;
case "flower-spin":
content = (
<svg
viewBox="0 0 64 64"
className={`${containerSize} ${primaryColorClass} ${className}`}
fill="currentColor"
style={{
transformBox: "fill-box",
transformOrigin: "center",
animation: "loader-flower-bloom 2.4s ease-in-out infinite",
}}
>
<style>{`
@keyframes loader-flower-bloom {
0%, 100% { transform: scale(0.85) rotate(0deg); }
50% { transform: scale(1.05) rotate(180deg); }
}
@keyframes loader-flower-petal {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
`}</style>
{[0, 60, 120, 180, 240, 300].map((a, i) => (
<circle
key={a}
cx="32"
cy="24"
r="12"
transform={`rotate(${a} 32 32)`}
style={{
animation: `loader-flower-petal 1.2s ease-in-out ${i * 0.15}s infinite`,
}}
/>
))}
<circle cx="32" cy="32" r="5" fill="none" stroke="currentColor" strokeWidth="2.5" />
</svg>
);
break;
case "bouncing-ball":
content = (
<div className={`relative ${className}`} style={{ aspectRatio: 2 }}>
<style>{`
@keyframes loader-ball-ground {
to { background-position: -125% 100%; }
}
@keyframes loader-ball-bounce {
0%, 2% { bottom: 0%; }
50% { bottom: 55%; }
98%, to { bottom: 0.1%; }
}
`}</style>
<div
className="absolute bottom-0 left-0 right-0 h-[3px]"
style={{
background: "linear-gradient(90deg, currentColor 50%, transparent 0) -25% 100% / 50% 3px repeat-x border-box",
animation: "loader-ball-ground 0.75s linear infinite",
}}
/>
<div
className="absolute bottom-0 w-[18%] aspect-square rounded-full bg-current"
style={{
left: "41%",
animation: "loader-ball-bounce 0.75s cubic-bezier(0, 900, 1, 900) infinite",
}}
/>
</div>
);
break;
case "walking-steps":
content = (
<div className={`relative ${containerSize} ${className}`}>
<style>{`
@keyframes loader-walk-leg-1 {
0%, 10% { transform: translateY(0); background-position: 0 calc(var(--s, 0) * -100%), calc(var(--s, 0) * 100%) 50%; }
33% { transform: translateY(-20%); background-position: 0 calc(var(--s, 0) * -100%), calc(var(--s, 0) * 100%) 50%; }
66% { transform: translateY(-20%); background-position: 0 calc(var(--s, 0) * -100%), calc(var(--s, 0) * 100% + 100%) 50%; }
90%, 100% { transform: translateY(0); background-position: 0 calc(var(--s, 0) * -100%), calc(var(--s, 0) * 100% + 100%) 50%; }
}
@keyframes loader-walk-leg-2 {
0%, 10% { transform: translateY(0); background-position: 0 calc(-1 * -100%), calc(-1 * 100%) 50%; }
33% { transform: translateY(20%); background-position: 0 calc(-1 * -100%), calc(-1 * 100%) 50%; }
66% { transform: translateY(20%); background-position: 0 calc(-1 * -100%), calc(-1 * 100% + 100%) 50%; }
90%, 100% { transform: translateY(0); background-position: 0 calc(-1 * -100%), calc(-1 * 100% + 100%) 50%; }
}
`}</style>
<div
className="absolute inset-0"
style={{
background: "linear-gradient(currentColor 0 0) 0 0 / 100% calc(100% / 3), repeating-linear-gradient(90deg, currentColor 0 25%, transparent 0 50%) 0 50% / calc(4 * 100% / 3) calc(100% / 3)",
backgroundRepeat: "no-repeat",
animation: "loader-walk-leg-1 2s infinite",
}}
/>
<div
className="absolute inset-0"
style={{
background: "linear-gradient(currentColor 0 0) 0 -100% / 100% calc(100% / 3), repeating-linear-gradient(90deg, currentColor 0 25%, transparent 0 50%) -100% 50% / calc(4 * 100% / 3) calc(100% / 3)",
backgroundRepeat: "no-repeat",
animation: "loader-walk-leg-2 2s infinite",
}}
/>
</div>
);
break;
case "corner-chase":
content = (
<div className={`relative ${containerSize} ${className}`}>
<style>{`
@keyframes loader-chase-1 {
0% { inset: 0 35% 35% 0; }
12.5% { inset: 0 35% 0 0; }
25% { inset: 35% 35% 0 0; }
37.5% { inset: 35% 0 0 0; }
50% { inset: 35% 0 0 35%; }
62.5% { inset: 0 0 0 35%; }
75% { inset: 0 0 35% 35%; }
87.5% { inset: 0 0 35% 0; }
100% { inset: 0 35% 35% 0; }
}
@keyframes loader-chase-2 {
0% { inset: 0 35% 35% 0; }
12.5% { inset: 0 35% 0 0; }
25% { inset: 35% 35% 0 0; }
37.5% { inset: 35% 0 0 0; }
50% { inset: 35% 0 0 35%; }
62.5% { inset: 0 0 0 35%; }
75% { inset: 0 0 35% 35%; }
87.5% { inset: 0 0 35% 0; }
100% { inset: 0 35% 35% 0; }
}
`}</style>
<div
className="absolute rounded-full"
style={{
boxShadow: "0 0 0 3px inset currentColor",
animation: "loader-chase-1 2.5s infinite",
}}
/>
<div
className="absolute rounded-full"
style={{
boxShadow: "0 0 0 3px inset currentColor",
animation: "loader-chase-2 2.5s infinite",
animationDelay: "-1.25s",
}}
/>
</div>
);
break;
}
return (
<>
{keyframes ? <style>{keyframes}</style> : null}
{content}
</>
);
};