Spin
Scale
Blink
Hourglass
Shimmer
Generating...
Wave
Generating...
Blink
Generating...
Text Shimmer
Tailwind config
{
"keyframes": {
"shimmer": {
"0%": {
"transform": "translateX(-100%)"
},
"100%": {
"transform": "translateX(100%)"
}
},
"wave": {
"0%, 100%": {
"transform": "translateY(0px) scale(1)"
},
"50%": {
"transform": "translateY(-2px) scale(1.2)"
}
},
"blink": {
"50%": {
"opacity": "0.5"
}
},
"hourglass": {
"0%": {
"transform": "rotate(0deg)"
},
"30%": {
"transform": "rotate(180deg)"
},
"70%": {
"transform": "rotate(180deg)"
},
"100%": {
"transform": "rotate(0deg)"
}
},
"scale-up-down": {
"0%, 100%": {
"transform": "scale(1)"
},
"50%": {
"transform": "scale(1.1)"
}
},
"text-shimmer": {
"0%, 90%, 100%": {
"background-position": "calc(-100% - var(--shimmer-width)) 0"
},
"30%, 60%": {
"background-position": "calc(100% + var(--shimmer-width)) 0"
}
}
},
"animation": {
"shimmer": "shimmer 1.6s infinite",
"wave": "wave 1.2s ease-in-out infinite",
"blink": "blink 1.6s ease-in-out infinite",
"hourglass": "hourglass 2s ease-in-out infinite",
"text-shimmer": "text-shimmer 6.4s infinite",
"scale-up-down": "scale-up-down 1.2s ease-in-out infinite"
}
}
Code
import { PiSpinnerGapBold } from "react-icons/pi";
import * as React from "react";
import { cn } from "@/lib/utils";
type TextVariant = "wave" | "shimmer" | "text-shimmer" | "blink";
type SpinnerVariant = "spin" | "scale-up-down" | "blink" | "hourglass";
interface LoaderTextProps extends React.HTMLAttributes<HTMLSpanElement> {
variant?: TextVariant;
children?: React.ReactNode;
width?: number;
}
interface LoaderIconProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: SpinnerVariant;
children?: React.ReactNode;
}
const textVariants: Record<TextVariant, string> = {
wave: "animate-wave",
shimmer:
"relative overflow-hidden bg-muted text-transparent rounded select-none",
blink: "animate-blink",
"text-shimmer": "animate-text-shimmer",
};
const spinnerVariants: Record<SpinnerVariant, string> = {
spin: "animate-spin",
"scale-up-down": "animate-scale-up-down",
blink: "animate-blink",
hourglass: "animate-hourglass",
};
const shimmerOverlay = (
<span className="animate-shimmer via-primary/5 pointer-events-none absolute inset-0 bg-linear-to-r from-transparent to-transparent" />
);
const LoaderText = React.forwardRef<HTMLSpanElement, LoaderTextProps>(
(
{ variant = "shimmer", children, width = 100, className, ...props },
ref,
) => {
const content = typeof children === "string" ? children : "Loading...";
if (variant === "wave" && typeof children === "string") {
return (
<span
ref={ref}
className={cn(
"text-muted-foreground inline-block text-sm",
className,
)}
{...props}
>
{content.split("").map((char, index) => (
<span
key={index}
className="animate-wave inline-block"
style={{ animationDelay: `${index * 0.1}s` }}
>
{char === " " ? "\u00A0" : char}
</span>
))}
</span>
);
}
if (variant === "text-shimmer" && typeof children === "string") {
return (
<span
ref={ref}
style={{ "--shimmer-width": `${width}px` } as React.CSSProperties}
className={cn(
"animate-text-shimmer mx-auto text-sm text-zinc-600/40 dark:text-zinc-400/40",
"bg-size-[var(--shimmer-width)_100%] bg-clip-text bg-position-[0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
"via-primary bg-linear-to-r from-transparent via-50% to-transparent",
className,
)}
{...props}
>
{children}
</span>
);
}
return (
<span
ref={ref}
className={cn(
"text-muted-foreground inline-block text-sm",
textVariants[variant],
variant === "shimmer" && "relative",
className,
)}
{...props}
>
{variant === "shimmer" ? shimmerOverlay : children}
</span>
);
},
);
LoaderText.displayName = "LoaderText";
const LoaderIcon = React.forwardRef<HTMLDivElement, LoaderIconProps>(
({ variant = "spin", children, className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"inline-flex items-center justify-center",
spinnerVariants[variant],
variant === "scale-up-down" && `[&>svg]:${spinnerVariants[variant]}`,
className,
)}
{...props}
>
{children ?? <PiSpinnerGapBold className="text-foreground size-4" />}
</div>
);
},
);
LoaderIcon.displayName = "LoaderIcon";
export { LoaderText, LoaderIcon };