Shimmer Skeleton

Loading skeleton with a soft diagonal shimmer wave sweeping across, in 4 composable variants

skeletonloadershimmerplaceholder
Preview

Source Code

"use client";

const SHIMMER_STYLE = `
@keyframes shimmer-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(200%); }
}
`;

export interface ShimmerSkeletonProps {
  /** Variant. Default: 'line' */
  variant?: "line" | "block" | "avatar" | "card";
  /** Width (CSS). Defaults per variant. */
  width?: string | number;
  /** Height (CSS). Defaults per variant. */
  height?: string | number;
  /** Shimmer duration in seconds. Default: 1.8 */
  duration?: number;
  /** Base bg color. Default: 'rgba(255,255,255,0.06)' */
  baseColor?: string;
  /** Highlight color. Default: 'rgba(255,255,255,0.12)' */
  highlightColor?: string;
  className?: string;
}

type VariantConfig = {
  width: string;
  height: string;
  borderRadius: string;
};

const VARIANT_DEFAULTS: Record<string, VariantConfig> = {
  line:   { width: "100%",  height: "14px",  borderRadius: "4px" },
  block:  { width: "100%",  height: "120px", borderRadius: "12px" },
  avatar: { width: "48px",  height: "48px",  borderRadius: "9999px" },
  card:   { width: "320px", height: "180px", borderRadius: "16px" },
};

function toCss(value: string | number): string {
  return typeof value === "number" ? `${value}px` : value;
}

function SkeletonBase({
  width,
  height,
  borderRadius,
  duration,
  baseColor,
  highlightColor,
  className = "",
}: {
  width: string;
  height: string;
  borderRadius: string;
  duration: number;
  baseColor: string;
  highlightColor: string;
  className?: string;
}) {
  return (
    <div
      className={className}
      style={{
        position: "relative",
        overflow: "hidden",
        width,
        height,
        borderRadius,
        background: baseColor,
        flexShrink: 0,
      }}
    >
      <div
        style={{
          position: "absolute",
          inset: 0,
          background: `linear-gradient(110deg, transparent 30%, ${highlightColor} 50%, transparent 70%)`,
          animation: `shimmer-sweep ${duration}s ease-in-out infinite`,
        }}
      />
    </div>
  );
}

export default function ShimmerSkeleton({
  variant = "line",
  width,
  height,
  duration = 1.8,
  baseColor = "rgba(255,255,255,0.06)",
  highlightColor = "rgba(255,255,255,0.12)",
  className = "",
}: ShimmerSkeletonProps) {
  const defaults = VARIANT_DEFAULTS[variant];

  const resolvedWidth  = width  != null ? toCss(width)  : defaults.width;
  const resolvedHeight = height != null ? toCss(height) : defaults.height;
  const borderRadius   = defaults.borderRadius;

  if (variant === "card") {
    return (
      <>
        <style dangerouslySetInnerHTML={{ __html: SHIMMER_STYLE }} />
        <div
          className={className}
          style={{
            position: "relative",
            overflow: "hidden",
            width: resolvedWidth,
            height: resolvedHeight,
            borderRadius,
            background: baseColor,
            flexShrink: 0,
            display: "flex",
            flexDirection: "column",
            justifyContent: "flex-end",
            padding: "16px",
            gap: "8px",
          }}
        >
          {/* Global shimmer sweep on the card wrapper */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              background: `linear-gradient(110deg, transparent 30%, ${highlightColor} 50%, transparent 70%)`,
              animation: `shimmer-sweep ${duration}s ease-in-out infinite`,
            }}
          />
          {/* Inner lines — title + 2 body lines */}
          <div style={{ position: "relative", zIndex: 1, display: "flex", flexDirection: "column", gap: "8px" }}>
            <div
              style={{
                width: "60%",
                height: "14px",
                borderRadius: "4px",
                background: highlightColor,
              }}
            />
            <div
              style={{
                width: "90%",
                height: "12px",
                borderRadius: "4px",
                background: highlightColor,
              }}
            />
            <div
              style={{
                width: "75%",
                height: "12px",
                borderRadius: "4px",
                background: highlightColor,
              }}
            />
          </div>
        </div>
      </>
    );
  }

  return (
    <>
      <style dangerouslySetInnerHTML={{ __html: SHIMMER_STYLE }} />
      <SkeletonBase
        width={resolvedWidth}
        height={resolvedHeight}
        borderRadius={borderRadius}
        duration={duration}
        baseColor={baseColor}
        highlightColor={highlightColor}
        className={className}
      />
    </>
  );
}