Loader

Animated loaders including text shimmer, wave, hourglass, and icons.

Spin

Scale

Blink

Hourglass

Icon Loader

Shimmer

Generating...

Wave

Generating...

Blink

Generating...

Text Shimmer

Text Loader

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 };