Stairs

No dependencies
Code format
React + Tailwind
const T = 0.78;
const W = 20; // tread
const H = 14; // riser
const Y = 52; // the tread the ball stands on
const BALL = 6.5;
const STAIRS = `M-20 ${Y + 3 * H}${' h20 v-14'.repeat(9)}`;

const css = `
  .loader-stairs { --ink: light-dark(#373a40, #f1f2f3); --step: light-dark(#6d7179, #7d8087); display: inline-block; font-size: 124px; width: 1em; height: 0.8em; }
  .loader-stairs svg { display: block; width: 100%; height: 100%; mask-image: linear-gradient(55deg, transparent 6%, #000 34%, #000 66%, transparent 94%); }
  /* The ball hops straight up and comes down where it took off; the stairs slide one step
     down and back under it while it is in the air, so it lands a step higher. */
  .loader-stairs__steps { fill: none; stroke: var(--step); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; animation: loader-stairs-slide ${T}s infinite; }
  .loader-stairs__ball { fill: var(--ink); transform-box: view-box; transform-origin: 50px ${Y - 1.5}px; animation: loader-stairs-hop ${T}s infinite; }
  @keyframes loader-stairs-slide {
    0%, 10% { transform: translate(0, 0); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
    58%, 100% { transform: translate(-${W}px, ${H}px); }
  }
  @keyframes loader-stairs-hop {
    0% { transform: translateY(0) scale(1, 1); animation-timing-function: ease-out; }
    8% { transform: translateY(0) scale(1.18, 0.8); animation-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); }
    34% { transform: translateY(-23px) scale(0.94, 1.08); animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.4); }
    58% { transform: translateY(0) scale(1.2, 0.8); animation-timing-function: ease-out; }
    70% { transform: translateY(0) scale(0.96, 1.04); }
    82%, 100% { transform: translateY(0) scale(1, 1); }
  }
  @media (prefers-reduced-motion: reduce) {
    .loader-stairs__steps, .loader-stairs__ball { animation: none; }
  }
`;

export function Preview() {
  return (
    <>
      <style href="kit-loaders-stairs" precedence="kit">
        {css}
      </style>
      <div className="loader-stairs" role="status" aria-label="Loading">
        <svg viewBox="0 0 100 80" aria-hidden="true">
          <path className="loader-stairs__steps" d={STAIRS} />
          <circle className="loader-stairs__ball" cx="50" cy={Y - 1.5 - BALL} r={BALL} />
        </svg>
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/loader-stairs
Source on GitHub

About this component

A ball hops up a staircase that slides down under it, so it climbs forever without going anywhere.

Dependencies
None
Difficulty
Intermediate
Triggers
continuous, loop, spring
Techniques
svg, keyframes, transform only
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion the ball rests on a step.

More loaders

See all
Stamp
Stretch
Swap
Tape Measure