Micro-Interaction Loaders
Premium status indicators with GSAP-powered physics.
Component Based
01
Magnifying Glass
npx shadcn@latest add https://aftershade.pages.dev/registry/magnifying-glass-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
interface MagnifyingGlassLoaderProps {
className?: string;
color?: string;
}
export function MagnifyingGlassLoader({
className,
color = "currentColor"
}: MagnifyingGlassLoaderProps) {
const glassRef = useRef<SVGSVGElement>(null);
const lensRef = useRef<SVGCircleElement>(null);
const handleRef = useRef<SVGPathElement>(null);
const shineRef = useRef<SVGPathElement>(null);
useEffect(() => {
if (!glassRef.current || !lensRef.current || !handleRef.current || !shineRef.current) return;
const tl = gsap.timeline({ repeat: -1 });
gsap.set(glassRef.current, { transformOrigin: "center center" });
gsap.set([lensRef.current, handleRef.current], { transformOrigin: "center center" });
tl.to(glassRef.current, { rotation: -15, x: -15, y: -5, duration: 0.8, ease: "power2.inOut" })
.to(glassRef.current, { rotation: 10, x: 15, y: 5, duration: 1.2, ease: "power2.inOut" })
.to(glassRef.current, { rotation: 0, x: 0, y: 0, duration: 0.8, ease: "elastic.out(1, 0.5)" });
gsap.to(lensRef.current, { scale: 1.1, strokeWidth: 2.5, duration: 0.6, repeat: -1, yoyo: true, ease: "sine.inOut" });
const shineTl = gsap.timeline({ repeat: -1, repeatDelay: 1.5 });
shineTl.fromTo(shineRef.current, { x: -10, opacity: 0, scale: 0.5 }, { x: 10, opacity: 1, scale: 1.2, duration: 0.6, ease: "power2.out" })
.to(shineRef.current, { opacity: 0, duration: 0.2 }, "-=0.2");
return () => { tl.kill(); shineTl.kill(); };
}, []);
return (
<div className={cn("relative w-24 h-24 flex items-center justify-center", className)}>
<svg ref={glassRef} width="64" height="64" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary-1">
<circle ref={lensRef} cx="11" cy="11" r="8" className="transition-colors" />
<path ref={handleRef} d="m21 21-4.3-4.3" />
<path ref={shineRef} d="M8 8a4 4 0 0 1 3-1" stroke="white" strokeWidth="1.5" strokeOpacity="0.8" className="blur-[0.5px]" />
</svg>
</div>
);
}02
Puzzle Loader
npx shadcn@latest add https://aftershade.pages.dev/registry/puzzle-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function PuzzleLoader({ className, gap = 4, color = "bg-primary-1" }: any) {
const containerRef = useRef<HTMLDivElement>(null);
const boxesRef = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const boxes = boxesRef.current;
if (boxes.length !== 4) return;
gsap.set(boxes, { x: 0, y: 0, scale: 1, borderRadius: "20%" });
const moveDist = 24;
const tl = gsap.timeline({ repeat: -1, repeatDelay: 0.2 });
tl.to(boxes, { scale: 0.85, borderRadius: "40%", duration: 0.3, ease: "back.in(2)" });
tl.to(boxes[0], { x: moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1")
.to(boxes[1], { y: moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1")
.to(boxes[3], { x: -moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1")
.to(boxes[2], { y: -moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1");
tl.to(boxes, { scale: 1, borderRadius: "20%", duration: 0.3, ease: "back.out(2)" });
tl.to(containerRef.current, { rotation: 90, duration: 0.6, ease: "expo.inOut", delay: 0.2 });
return () => { tl.kill(); };
}, []);
return (
<div className={cn("w-24 h-24 flex items-center justify-center", className)}>
<div ref={containerRef} className="relative w-12 h-12">
<div className="absolute top-0 left-0 grid grid-cols-2 gap-1 w-full h-full">
{[0, 1, 2, 3].map((i) => (
<div key={i} ref={(el) => { boxesRef.current[i] = el; }} className={cn("w-5 h-5 rounded-md", color)} />
))}
</div>
</div>
</div>
);
}03
Progress Bar
npx shadcn@latest add https://aftershade.pages.dev/registry/progress-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
interface ProgressLoaderProps {
className?: string;
width?: number | string;
height?: number | string;
color?: string;
trackColor?: string;
}
export function ProgressLoader({
className,
width = "100%",
height = 4,
color = "bg-primary-1",
trackColor = "bg-secondary"
}: ProgressLoaderProps) {
const barRef = useRef<HTMLDivElement>(null);
const glowRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!barRef.current || !glowRef.current) return;
const tl = gsap.timeline({ repeat: -1 });
gsap.set(barRef.current, { xPercent: -100, scaleX: 0.2, transformOrigin: "left center" });
gsap.set(glowRef.current, { opacity: 0 });
tl.to(barRef.current, { xPercent: -20, scaleX: 0.6, duration: 1, ease: "power3.inOut" })
.to(glowRef.current, { opacity: 0.8, duration: 0.3 }, "<");
tl.to(barRef.current, { xPercent: 20, scaleX: 0.8, duration: 1.2, ease: "linear" });
tl.to(barRef.current, { xPercent: 105, scaleX: 0.1, duration: 0.8, ease: "power4.out" })
.to(glowRef.current, { opacity: 0, duration: 0.4 }, "-=0.2");
return () => { tl.kill(); };
}, []);
return (
<div className={cn("w-full max-w-xs", className)}>
<div className={cn("relative overflow-hidden rounded-full", trackColor)} style={{ height, width }}>
<div ref={barRef} className={cn("absolute top-0 left-0 h-full w-full rounded-full origin-left", color)}>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent w-full h-full opacity-50" />
</div>
</div>
</div>
);
}04
Multi-Ring Spinner
npx shadcn@latest add https://aftershade.pages.dev/registry/spinner-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function SpinnerLoader({ className, size = 64, color = "currentColor" }: any) {
const svgRef = useRef<SVGSVGElement>(null);
const ring1Ref = useRef<SVGCircleElement>(null);
const ring2Ref = useRef<SVGCircleElement>(null);
const ring3Ref = useRef<SVGCircleElement>(null);
useEffect(() => {
if (!ring1Ref.current) return;
const rings = [ring1Ref.current, ring2Ref.current, ring3Ref.current];
gsap.set(rings, { transformOrigin: "center center" });
gsap.to(svgRef.current, { rotation: 360, duration: 20, repeat: -1, ease: "none" });
gsap.to(ring1Ref.current, { rotation: 360, duration: 1.5, repeat: -1, ease: "power1.inOut" });
gsap.to(ring2Ref.current, { rotation: 360, duration: 2, repeat: -1, ease: "elastic.inOut(1.2, 0.7)", delay: 0.1 });
gsap.to(ring3Ref.current, { rotation: -360, duration: 3, repeat: -1, ease: "sine.inOut" });
gsap.to(rings, { strokeWidth: 4, duration: 1, yoyo: true, repeat: -1, ease: "sine.inOut", stagger: 0.2 });
}, []);
return (
<div className={cn("relative flex items-center justify-center", className)} style={{ width: size, height: size }}>
<svg ref={svgRef} viewBox="0 0 100 100" fill="none" className="w-full h-full text-primary-1">
<circle ref={ring3Ref} cx="50" cy="50" r="40" stroke={color} strokeWidth="1" strokeOpacity="0.3" strokeDasharray="10 5" strokeLinecap="round" />
<circle ref={ring2Ref} cx="50" cy="50" r="32" stroke={color} strokeWidth="2" strokeOpacity="0.6" strokeDasharray="50 150" strokeLinecap="round" />
<circle ref={ring1Ref} cx="50" cy="50" r="24" stroke={color} strokeWidth="3" strokeDasharray="100 200" strokeLinecap="round" className="drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
</svg>
</div>
);
}05
Orbital Dots
npx shadcn@latest add https://aftershade.pages.dev/registry/orbital-dots-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function OrbitalDotsLoader({ className, size = 40, color = "bg-primary-1" }: any) {
const dot1Ref = useRef<HTMLDivElement>(null);
const dot2Ref = useRef<HTMLDivElement>(null);
const centerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!dot1Ref.current) return;
gsap.to(dot1Ref.current, { rotation: 360, duration: 1.5, repeat: -1, ease: "linear", transformOrigin: "20px 20px" });
gsap.to(dot2Ref.current, { rotation: -360, duration: 2.5, repeat: -1, ease: "linear", transformOrigin: "-10px -10px" });
gsap.to(centerRef.current, { scale: 1.5, opacity: 0.5, duration: 0.8, yoyo: true, repeat: -1, ease: "sine.inOut" });
}, []);
return (
<div className={cn("relative flex items-center justify-center", className)} style={{ width: size * 3, height: size * 3 }}>
<div ref={centerRef} className={cn("w-3 h-3 rounded-full absolute", color)} />
<div ref={dot1Ref} className="absolute w-full h-full flex items-center justify-center"><div className={cn("w-3 h-3 rounded-full absolute -top-4", color)} /></div>
<div ref={dot2Ref} className="absolute w-full h-full flex items-center justify-center"><div className={cn("w-2 h-2 rounded-full absolute -bottom-8 opacity-60", color)} /></div>
</div>
);
}06
Wave Bar
npx shadcn@latest add https://aftershade.pages.dev/registry/wave-bar-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function WaveBarLoader({ className, count = 5, color = "bg-primary-1" }: any) {
const barsRef = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
if (barsRef.current.length === 0) return;
gsap.to(barsRef.current, { scaleY: 2.5, duration: 0.4, repeat: -1, yoyo: true, ease: "sine.inOut", stagger: { each: 0.1, from: "center" } });
}, []);
return (
<div className={cn("flex items-center gap-1 h-12", className)}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} ref={(el) => { barsRef.current[i] = el; }} className={cn("w-1.5 h-4 rounded-full", color)} />
))}
</div>
);
}07
Grid Pulse
npx shadcn@latest add https://aftershade.pages.dev/registry/grid-pulse-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function GridPulseLoader({ className, color = "bg-primary-1" }: any) {
const dotsRef = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
gsap.to(dotsRef.current, { opacity: 0.2, scale: 0.5, duration: 0.8, repeat: -1, yoyo: true, ease: "power1.inOut", stagger: { grid: [3, 3], from: "random", amount: 1 } });
}, []);
return (
<div className={cn("grid grid-cols-3 gap-1", className)}>
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} ref={(el) => { dotsRef.current[i] = el; }} className={cn("w-3 h-3 rounded-sm", color)} />
))}
</div>
);
}08
Quantum Core
npx shadcn@latest add https://aftershade.pages.dev/registry/quantum-ring-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function QuantumRingLoader({ className, size = 200, color = "text-primary-1" }: any) {
const ring1Ref = useRef<SVGCircleElement>(null);
const ring2Ref = useRef<SVGCircleElement>(null);
const coreRef = useRef<SVGCircleElement>(null);
useEffect(() => {
if (!ring1Ref.current) return;
gsap.to(ring1Ref.current, { rotation: 360, transformOrigin: "center center", duration: 2, repeat: -1, ease: "linear" });
gsap.to(ring2Ref.current, { rotation: -360, transformOrigin: "center center", duration: 2.5, repeat: -1, ease: "linear" });
gsap.to([ring1Ref.current, ring2Ref.current], { scale: 1.1, transformOrigin: "center center", strokeWidth: 1.5, duration: 1, repeat: -1, yoyo: true, ease: "sine.inOut" });
gsap.to(coreRef.current, { scale: 1.5, opacity: 0.8, duration: 0.5, repeat: -1, yoyo: true, ease: "power2.inOut" });
}, []);
return (
<div className={cn("relative flex items-center justify-center", className)} style={{ width: 64, height: 64 }}>
<svg width="64" height="64" viewBox="0 0 100 100" fill="none" className={color}>
<circle ref={coreRef} cx="50" cy="50" r="8" fill="currentColor" className="opacity-50 blur-[2px]" />
<circle ref={ring1Ref} cx="50" cy="50" r="30" stroke="currentColor" strokeWidth="2" fill="none" className="opacity-80" style={{ transformBox: "fill-box" }} />
<circle ref={ring2Ref} cx="50" cy="50" r="30" stroke="currentColor" strokeWidth="2" fill="none" className="opacity-60" style={{ transform: "rotate(90deg)", transformBox: "fill-box" }} />
</svg>
</div>
);
}09
Code Stream
npx shadcn@latest add https://aftershade.pages.dev/registry/code-typer-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function CodeTyperLoader({ className, color = "bg-primary-1" }: any) {
const linesRef = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
if (linesRef.current.length === 0) return;
const tl = gsap.timeline({ repeat: -1, repeatDelay: 0.5 });
tl.to(linesRef.current, { width: (i) => ["60%", "80%", "40%", "90%"][i], duration: 0.4, stagger: 0.1, ease: "steps(4)" });
tl.to(linesRef.current, { opacity: 0.5, duration: 0.2, yoyo: true, repeat: 1, ease: "power1.inOut" });
tl.to(linesRef.current, { width: 0, duration: 0.2, stagger: 0.05, ease: "power2.in" });
}, []);
return (
<div className={cn("flex flex-col gap-2 w-16", className)}>
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} ref={(el) => { linesRef.current[i] = el; }} className={cn("h-2 rounded-full w-0", color)} />
))}
</div>
);
}10
Liquid Goo
npx shadcn@latest add https://aftershade.pages.dev/registry/liquid-blob-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";
export function LiquidBlobLoader({ className, color = "bg-primary-1" }: any) {
const blob1Ref = useRef<HTMLDivElement>(null);
const blob2Ref = useRef<HTMLDivElement>(null);
const filterId = React.useId();
useEffect(() => {
if (!blob1Ref.current) return;
const tl = gsap.timeline({ repeat: -1, yoyo: true });
tl.to(blob1Ref.current, { x: -15, scale: 0.8, duration: 1.2, ease: "power2.inOut" }, 0);
tl.to(blob2Ref.current, { x: 15, scale: 0.8, duration: 1.2, ease: "power2.inOut" }, 0);
gsap.to([blob1Ref.current, blob2Ref.current], { rotation: 180, duration: 2.4, repeat: -1, yoyo: true, ease: "sine.inOut" });
}, []);
return (
<div className={cn("relative w-24 h-24 flex items-center justify-center", className)}>
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<filter id={`goo-${filterId}`}>
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="goo" />
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</svg>
<div className="relative w-full h-full flex items-center justify-center" style={{ filter: `url(#goo-${filterId})` }}>
<div ref={blob1Ref} className={cn("absolute w-12 h-12 rounded-full blur-sm", color)} />
<div ref={blob2Ref} className={cn("absolute w-12 h-12 rounded-full blur-sm", color)} />
</div>
</div>
);
}