Loaders

A collection of loaders

Installation

Install Dependencies

npm i clsx tailwind-merge framer-motion

Create @/utils/cn.ts file

import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Loaders Component

"use client";
import { cn } from "@/lib/utils";
import React from "react";

interface LoaderProps {
  size?: "sm" | "md" | "lg" | "xl" | string;
  className?: string;
  color?: string;
  speed?: "default" | "slow" | "slowest" | "fast";
}

const sizeMap = {
  sm: "20px",
  md: "30px",
  lg: "40px",
  xl: "50px"
};

const dotSizeMap = {
  sm: 10,
  md: 12,
  lg: 14,
  xl: 16
};

const boxSizeMap = {
  sm: 10,
  md: 20,
  lg: 30,
  xl: 40
};

const speedMap = {
  slow: "2s",
  slowest: "3s",
  fast: ".5s",
  default: "1s"
};

const getSizeValue = (size: LoaderProps["size"]) => sizeMap[size as keyof typeof sizeMap] || size;
const getDotSizeValue = (size: LoaderProps["size"]) => dotSizeMap[size as keyof typeof dotSizeMap] || 4;
const getBoxSizeValue = (size: LoaderProps["size"]) => boxSizeMap[size as keyof typeof boxSizeMap] || 20;
const getSpeedValue = (speed: LoaderProps["speed"]) => speedMap[speed as keyof typeof speedMap] || "1s";

const DefaultLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div
      className={cn("rounded-full border-2 border-solid", className)}
      style={{
        height: sizeValue,
        width: sizeValue,
        animation: `spin ${speedValue} linear infinite`,
        borderLeftColor: color,
        borderRightColor: color,
        borderBottomColor: color,
        borderTopColor: "transparent"
      }}
    ></div>
  );
};

const CrescentLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div className={cn("relative", className)}>
      <div
        style={{ height: sizeValue, width: sizeValue, borderRadius: "50%", border: "2px solid", borderColor: color, opacity: 0.25 }}
      ></div>
      <div
        style={{
          position: "absolute",
          left: 0,
          top: 0,
          height: sizeValue,
          width: sizeValue,
          animation: `spin ${speedValue} linear infinite`,
          borderRadius: "50%",
          borderTop: `2px solid ${color}`
        }}
      ></div>
    </div>
  );
};

const ArrowsLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div className={className}>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
        viewBox="0 0 16 16"
        style={{
          height: sizeValue,
          width: sizeValue,
          animation: `spin ${speedValue} linear infinite`,
          color
        }}
      >
        <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z" />
        <path
          fillRule="evenodd"
          d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"
        />
      </svg>
    </div>
  );
};

const DashedLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div
      className={cn("rounded-full border-2 border-dashed", className)}
      style={{
        height: sizeValue,
        width: sizeValue,
        animation: `spin ${speedValue} linear infinite`,
        borderColor: color
      }}
    ></div>
  );
};

const DoubleCrescentLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div
      className={cn("rounded-full border-b-2 border-t-2", className)}
      style={{
        height: sizeValue,
        width: sizeValue,
        animation: `spin ${speedValue} linear infinite`,
        borderColor: color
      }}
    ></div>
  );
};

const CrescentSharpLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div className={cn("relative", className)}>
      <div
        style={{ height: sizeValue, width: sizeValue, borderRadius: "50%", border: "2px solid", borderColor: color, opacity: 0.25 }}
      ></div>
      <div
        style={{
          position: "absolute",
          left: 0,
          top: 0,
          height: sizeValue,
          width: sizeValue,
          animation: `spin ${speedValue} linear infinite`,
          borderRadius: "50%",
          border: "2px solid transparent",
          borderTopColor: color
        }}
      ></div>
    </div>
  );
};

const LinesLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <>
      <div
        className={cn("spinner grid rounded-full", className)}
        style={{
          width: sizeValue,
          height: sizeValue,
          animation: `s3 ${speedValue} steps(12) infinite`
        }}
      ></div>
      <style jsx>{`
        .spinner {
          background:
            linear-gradient(0deg, ${color}80 30%, #0000 0 70%, ${color}ff 0) 50%/8% 100%,
            linear-gradient(90deg, ${color}50 30%, #0000 0 70%, ${color}bf 0) 50%/100% 8%;
          background-repeat: no-repeat;
        }
        .spinner::before,
        .spinner::after {
          content: "";
          grid-area: 1/1;
          border-radius: 50%;
          background: inherit;
          opacity: 0.915;
          transform: rotate(30deg);
        }
        .spinner::after {
          opacity: 0.83;
          transform: rotate(60deg);
        }

        @keyframes s3 {
          100% {
            transform: rotate(1turn);
          }
        }
      `}</style>
    </>
  );
};

const CirclesLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div className={className}>
      <div
        className={`spinner grid rounded-full`}
        style={{
          animation: `s6 ${speedValue} steps(12) infinite`,
          width: sizeValue,
          height: sizeValue
        }}
      ></div>
      <style jsx>{`
        .spinner {
          -webkit-mask: conic-gradient(from 15deg, #0000, #000);
        }
        .spinner,
        .spinner:before,
        .spinner:after {
          background:
            radial-gradient(closest-side at 50% 12.5%, ${color} 96%, #0000) 50% 0/20% 80% repeat-y,
            radial-gradient(closest-side at 12.5% 50%, ${color} 96%, #0000) 0 50%/80% 20% repeat-x;
        }
        .spinner:before,
        .spinner:after {
          content: "";
          grid-area: 1/1;
          transform: rotate(30deg);
        }
        .spinner:after {
          transform: rotate(60deg);
        }

        @keyframes s6 {
          100% {
            transform: rotate(1turn);
          }
        }
      `}</style>
    </div>
  );
};

const DotsLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const dotSizeValue = getDotSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div className={cn("flex items-center justify-center space-x-2", className)}>
      {[0, 1, 2].map((index) => (
        <div
          key={index}
          style={{
            backgroundColor: color,
            animation: `pulse ${speedValue} infinite`,
            animationDelay: `${index * 0.3}s`,
            width: `${dotSizeValue}px`,
            height: `${dotSizeValue}px`,
            borderRadius: "50%"
          }}
        ></div>
      ))}
    </div>
  );
};

const BounceLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const dotSizeValue = getDotSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div className={cn("flex items-center justify-center space-x-2", className)}>
      {[0, 1, 2].map((index) => (
        <div
          key={index}
          style={{
            backgroundColor: color,
            animation: `bounce ${speedValue} infinite`,
            animationDelay: `${index * 0.2}s`,
            width: `${dotSizeValue}px`,
            height: `${dotSizeValue}px`,
            borderRadius: "50%"
          }}
        ></div>
      ))}
    </div>
  );
};

const TriplesLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6", speed = "default" }) => {
  const sizeValue = getSizeValue(size);
  const speedValue = getSpeedValue(speed);

  return (
    <div className={className} style={{ height: sizeValue, width: sizeValue }}>
      <svg viewBox="0 0 100 100">
        <defs>
          <filter id="shadow">
            <feDropShadow dx="0" dy="0" stdDeviation="1.5" floodColor={color} />
          </filter>
        </defs>
        <circle
          style={{
            fill: "transparent",
            stroke: color,
            strokeWidth: 7,
            strokeLinecap: "round",
            filter: "url(#shadow)",
            animation: `triples ${speedValue} infinite`
          }}
          cx="50"
          cy="50"
          r="45"
        />
      </svg>
      <style jsx>{`
        @keyframes triples {
          0% {
            stroke-dasharray: 1 98;
            stroke-dashoffset: -105;
          }
          50% {
            stroke-dasharray: 80 10;
            stroke-dashoffset: -160;
          }
          100% {
            stroke-dasharray: 1 98;
            stroke-dashoffset: -300;
          }
        }
      `}</style>
    </div>
  );
};

const BoxesLoader: React.FC<LoaderProps> = ({ size = "md", className, color = "#3bccf6" }) => {
  const boxSizeValue = getBoxSizeValue(size);

  return (
    <div className={className}>
      <div
        className={"relative animate-[opposites_2.5s_ease-out_0s_infinite]"}
        style={{
          height: `${Number(boxSizeValue) * 2}px`,
          width: `${Number(boxSizeValue) * 2}px`,
          animation: `opposites 2.5s ease-out 0s infinite`
        }}
      >
        <div
          className={"absolute left-0 origin-top-right transform animate-[trbl_2.5s_ease-out_0s_infinite]"}
          style={{
            backgroundColor: color,
            filter: "brightness(.9)",
            top: `${boxSizeValue}px`,
            left: 0,
            height: `${boxSizeValue}px`,
            width: `${boxSizeValue}px`
          }}
        ></div>
        <div
          className={"absolute top-0 origin-bottom-left transform animate-[trbl_2.5s_ease-out_0s_infinite]"}
          style={{
            backgroundColor: color,
            filter: "brightness(.3)",
            left: `${boxSizeValue}px`,
            height: `${boxSizeValue}px`,
            width: `${boxSizeValue}px`
          }}
        ></div>
        <div
          className={"absolute origin-top-left transform animate-[tlbr_2.5s_ease-out_0s_infinite]"}
          style={{
            backgroundColor: color,
            filter: "brightness(.7)",
            left: `${boxSizeValue}px`,
            top: `${boxSizeValue}px`,
            height: `${boxSizeValue}px`,
            width: `${boxSizeValue}px`
          }}
        ></div>
        <div
          className={"absolute left-0 top-0 origin-bottom-right transform animate-[tlbr_2.5s_ease-out_0s_infinite]"}
          style={{ backgroundColor: color, filter: "brightness(.5)", height: `${boxSizeValue}px`, width: `${boxSizeValue}px` }}
        ></div>
      </div>
      <style jsx>{`
        @keyframes tlbr {
          0% {
            transform: rotate(0);
          }
          20% {
            transform: rotate(90deg);
          }
          40% {
            transform: rotate(90deg);
          }
          60% {
            transform: rotate(0);
          }
        }
        @keyframes trbl {
          20% {
            transform: rotate(0);
          }
          40% {
            transform: rotate(90deg);
          }
          60% {
            transform: rotate(90deg);
          }
          80% {
            transform: rotate(0);
          }
        }
        @keyframes opposites {
          80% {
            transform: rotate(0deg);
          }
          100% {
            transform: rotate(360deg);
          }
        }
      `}</style>
    </div>
  );
};

// Main Loaders component that renders based on variant
const Loaders: React.FC<LoaderProps & { variant?: string }> = ({ variant = "default", size = "md", className, color, speed }) => {
  switch (variant) {
    case "crecent":
      return <CrescentLoader size={size} className={className} color={color} speed={speed} />;
    case "arrows":
      return <ArrowsLoader size={size} className={className} color={color} speed={speed} />;
    case "dashed":
      return <DashedLoader size={size} className={className} color={color} speed={speed} />;
    case "double-crecent":
      return <DoubleCrescentLoader size={size} className={className} color={color} speed={speed} />;
    case "crecent-sharp":
      return <CrescentSharpLoader size={size} className={className} color={color} speed={speed} />;
    case "lines":
      return <LinesLoader size={size} className={className} color={color} speed={speed} />;
    case "circles":
      return <CirclesLoader size={size} className={className} color={color} speed={speed} />;
    case "dots":
      return <DotsLoader size={size} className={className} color={color} speed={speed} />;
    case "bounce":
      return <BounceLoader size={size} className={className} color={color} speed={speed} />;
    case "triples":
      return <TriplesLoader size={size} className={className} color={color} speed={speed} />;
    case "boxes":
      return <BoxesLoader size={size} className={className} color={color} speed={speed} />;
    default:
      return <DefaultLoader size={size} className={className} color={color} speed={speed} />;
  }
};

export default Loaders;

Examples

Colors

Sizes

Speed