Loaders

Shape Trace Loader

A trio of SVG loaders (circle, triangle, square) that each trace their own outline with animated stroke-dashoffset, while a small dot orbits around them. No dependency needed.

Loading preview
Advertisement
Ad space reserved for a future placement

Source code

"use client";

interface ShapeTraceLoaderProps {
  pathColor?: string;
  dotColor?: string;
  duration?: string;
  label?: string;
  className?: string;
}

export function ShapeTraceLoader({
  pathColor = "#2f3545",
  dotColor = "#5628ee",
  duration = "3s",
  label = "Loading",
  className,
}: ShapeTraceLoaderProps) {
  return (
    <div
      role="status"
      aria-label={label}
      className={["shape-trace-loader-group", className].filter(Boolean).join(" ")}
      style={{
        ["--path" as string]: pathColor,
        ["--dot" as string]: dotColor,
        ["--duration" as string]: duration,
      }}
    >
      <div className="loader">
        <svg viewBox="0 0 80 80">
          <rect height={64} width={64} y={8} x={8} />
        </svg>
      </div>
      <div className="loader triangle">
        <svg viewBox="0 0 86 80">
          <polygon points="43 8 79 72 7 72" />
        </svg>
      </div>
      <div className="loader">
        <svg viewBox="0 0 80 80">
          <circle r={32} cy={40} cx={40} />
        </svg>
      </div>
      <style jsx>{`
        .shape-trace-loader-group {
          display: inline-flex;
        }

        .loader {
          width: 44px;
          height: 44px;
          position: relative;
          display: inline-block;
          margin: 0 16px;
        }

        .loader:before {
          content: "";
          width: 6px;
          height: 6px;
          border-radius: 50%;
          position: absolute;
          display: block;
          background: var(--dot);
          top: 37px;
          left: 19px;
          transform: translate(-18px, -18px);
          animation: dotRect var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
        }

        .loader svg {
          display: block;
          width: 100%;
          height: 100%;
        }

        .loader svg rect,
        .loader svg polygon,
        .loader svg circle {
          fill: none;
          stroke: var(--path);
          stroke-width: 10px;
          stroke-linejoin: round;
          stroke-linecap: round;
        }

        .loader svg polygon {
          stroke-dasharray: 145 76 145 76;
          stroke-dashoffset: 0;
          animation: pathTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
        }

        .loader svg rect {
          stroke-dasharray: 192 64 192 64;
          stroke-dashoffset: 0;
          animation: pathRect var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
        }

        .loader svg circle {
          stroke-dasharray: 150 50 150 50;
          stroke-dashoffset: 75;
          animation: pathCircle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
        }

        .loader.triangle {
          width: 48px;
        }

        .loader.triangle:before {
          left: 21px;
          transform: translate(-10px, -18px);
          animation: dotTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
        }

        @keyframes pathTriangle {
          33% {
            stroke-dashoffset: 74;
          }
          66% {
            stroke-dashoffset: 147;
          }
          100% {
            stroke-dashoffset: 221;
          }
        }

        @keyframes dotTriangle {
          33% {
            transform: translate(0, 0);
          }
          66% {
            transform: translate(10px, -18px);
          }
          100% {
            transform: translate(-10px, -18px);
          }
        }

        @keyframes pathRect {
          25% {
            stroke-dashoffset: 64;
          }
          50% {
            stroke-dashoffset: 128;
          }
          75% {
            stroke-dashoffset: 192;
          }
          100% {
            stroke-dashoffset: 256;
          }
        }

        @keyframes dotRect {
          25% {
            transform: translate(0, 0);
          }
          50% {
            transform: translate(18px, -18px);
          }
          75% {
            transform: translate(0, -36px);
          }
          100% {
            transform: translate(-18px, -18px);
          }
        }

        @keyframes pathCircle {
          25% {
            stroke-dashoffset: 125;
          }
          50% {
            stroke-dashoffset: 175;
          }
          75% {
            stroke-dashoffset: 225;
          }
          100% {
            stroke-dashoffset: 275;
          }
        }

        @media (prefers-reduced-motion: reduce) {
          .loader:before,
          .loader svg polygon,
          .loader svg rect,
          .loader svg circle {
            animation-duration: 6s;
          }
        }
      `}</style>
    </div>
  );
}

Usage

Import the component and use it like this.

import { ShapeTraceLoader } from "./ShapeTraceLoader";

export default function Example() {
  return <ShapeTraceLoader pathColor="#2f3545" dotColor="#5628ee" duration="3s" label="Loading" />;
}

Dependencies

No external dependencies.

Usage notes

Pass pathColor (outline), dotColor (orbiting dot), duration (CSS time string), and an optional label.