Loaders

Stairs Bounce Loader

A single-div CSS loading spinner: a bouncing ball climbs a set of steps drawn with layered box-shadows, no dependency needed.

Loading preview
Advertisement
Ad space reserved for a future placement

Source code

"use client";

interface StairsBounceLoaderProps {
  accentColor?: string;
  stepColor?: string;
  label?: string;
  className?: string;
}

export function StairsBounceLoader({
  accentColor = "#3f3cf2",
  stepColor = "#4b5563",
  label = "Loading",
  className,
}: StairsBounceLoaderProps) {
  return (
    <div
      role="status"
      aria-label={label}
      className={["stairs-bounce-loader-wrapper", className].filter(Boolean).join(" ")}
      style={{ ["--bounce-ball" as string]: accentColor, ["--bounce-step" as string]: stepColor }}
    >
      <div className="loader" />
      <style jsx>{`
        .loader {
          position: relative;
          width: 120px;
          height: 90px;
          margin: 0 auto;
        }

        .loader:before {
          content: "";
          position: absolute;
          bottom: 30px;
          left: 32px;
          height: 30px;
          width: 30px;
          border-radius: 50%;
          background: var(--bounce-ball);
          animation: loading-bounce 0.5s ease-in-out infinite alternate;
        }

        .loader:after {
          content: "";
          position: absolute;
          right: 0;
          top: 0;
          height: 7px;
          width: 45px;
          border-radius: 4px;
          box-shadow:
            0 5px 0 var(--bounce-step),
            -35px 50px 0 var(--bounce-step),
            -70px 95px 0 var(--bounce-step);
          animation: loading-step 1s ease-in-out infinite;
        }

        @keyframes loading-bounce {
          0% {
            transform: scale(1, 0.7);
          }

          40% {
            transform: scale(0.8, 1.2);
          }

          60% {
            transform: scale(1, 1);
          }

          100% {
            bottom: 140px;
          }
        }

        @keyframes loading-step {
          0% {
            box-shadow:
              0 10px 0 rgba(0, 0, 0, 0),
              0 10px 0 var(--bounce-step),
              -35px 50px 0 var(--bounce-step),
              -70px 90px 0 var(--bounce-step);
          }

          100% {
            box-shadow:
              0 10px 0 var(--bounce-step),
              -35px 50px 0 var(--bounce-step),
              -70px 90px 0 var(--bounce-step),
              -70px 90px 0 rgba(0, 0, 0, 0);
          }
        }

        @media (prefers-reduced-motion: reduce) {
          .loader:before,
          .loader:after {
            animation-duration: 2.5s;
          }
        }
      `}</style>
    </div>
  );
}

Usage

Import the component and use it like this.

import { StairsBounceLoader } from "./StairsBounceLoader";

export default function Example() {
  return <StairsBounceLoader accentColor="#3f3cf2" stepColor="#4b5563" label="Loading" />;
}

Dependencies

No external dependencies.

Usage notes

Pass accentColor (ball) and stepColor (steps) hex values, and an optional label for the accessible status announcement.