Yo-yo

No dependencies
Code format
React + Tailwind
const css = `
  /* Falls speeding up, sleeps at the end of the string, climbs back slowing down. The string
     stretches in the same keyframes, so it always ends at the axle. */
  @keyframes loader-yoyo-drop {
    0% { transform: translateY(0); animation-timing-function: cubic-bezier(0.4, 0, 0.9, 0.6); }
    42% { transform: translateY(52px); animation-timing-function: ease-out; }
    45% { transform: translateY(49px); animation-timing-function: ease-in; }
    48%, 60% { transform: translateY(52px); animation-timing-function: cubic-bezier(0.15, 0.6, 0.35, 1); }
    96%, 100% { transform: translateY(0); }
  }
  @keyframes loader-yoyo-string {
    0% { transform: scaleY(0.2353); animation-timing-function: cubic-bezier(0.4, 0, 0.9, 0.6); }
    42% { transform: scaleY(1); animation-timing-function: ease-out; }
    45% { transform: scaleY(0.9559); animation-timing-function: ease-in; }
    48%, 60% { transform: scaleY(1); animation-timing-function: cubic-bezier(0.15, 0.6, 0.35, 1); }
    96%, 100% { transform: scaleY(0.2353); }
  }
  @keyframes loader-yoyo-spin {
    0% { transform: rotate(0deg); animation-timing-function: ease-in; }
    42% { transform: rotate(720deg); animation-timing-function: linear; }
    60% { transform: rotate(1260deg); animation-timing-function: ease-out; }
    96%, 100% { transform: rotate(1800deg); }
  }
  /* The tug that wakes it. */
  @keyframes loader-yoyo-tug { 0%, 58% { transform: translateY(0); } 62% { transform: translateY(-4px); } 70%, 100% { transform: translateY(0); } }
  @keyframes loader-yoyo-shadow {
    0% { transform: scaleX(0.3); opacity: 0.2; animation-timing-function: cubic-bezier(0.4, 0, 0.9, 0.6); }
    42%, 60% { transform: scaleX(1); opacity: 0.7; animation-timing-function: cubic-bezier(0.15, 0.6, 0.35, 1); }
    96%, 100% { transform: scaleX(0.3); opacity: 0.2; }
  }
  .loader-yoyo { position: relative; width: 66px; height: 110px; }
  .loader-yoyo svg { width: 100%; height: 100%; overflow: visible; }
  .loader-yoyo__hand { animation: loader-yoyo-tug 2.4s ease-in-out infinite; }
  .loader-yoyo__finger { fill: light-dark(#5f626a, #a7abb2); }
  .loader-yoyo__knuckle { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 1; stroke-linecap: round; }
  .loader-yoyo__string { fill: light-dark(#50535b, #c6c8cd); transform-origin: 30px 10px; animation: loader-yoyo-string 2.4s infinite; }
  .loader-yoyo__drop { animation: loader-yoyo-drop 2.4s infinite; }
  .loader-yoyo__spin { animation: loader-yoyo-spin 2.4s infinite; }
  .loader-yoyo__shell { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.6; }
  .loader-yoyo__cap { fill: light-dark(#dbdde1, #2e3035); }
  .loader-yoyo__mark { fill: none; stroke: light-dark(#50535b, #c6c8cd); stroke-width: 2; stroke-linecap: round; }
  .loader-yoyo__axle { fill: light-dark(#42454c, #e5e6e9); }
  .loader-yoyo__shadow { fill: light-dark(#cbced3, #393c41); transform-origin: 30px 96px; animation: loader-yoyo-shadow 2.4s infinite; }
  @media (prefers-reduced-motion: reduce) {
    .loader-yoyo__hand, .loader-yoyo__string, .loader-yoyo__drop, .loader-yoyo__spin, .loader-yoyo__shadow { animation: none; }
  }
`;

export function YoyoLoader() {
  return (
    <>
      <style>
        {css}
      </style>
      <div className="loader-yoyo" role="status" aria-label="Loading">
        <svg viewBox="0 0 60 100" aria-hidden="true">
          <ellipse className="loader-yoyo__shadow" cx="30" cy="96" rx="12" ry="2" />
          <g className="loader-yoyo__hand">
            <rect className="loader-yoyo__string" x="29.4" y="10" width="1.2" height="68" />
            <g className="loader-yoyo__drop">
              <g transform="translate(30 26)">
                <g className="loader-yoyo__spin">
                  <circle className="loader-yoyo__shell" r="11" />
                  <circle className="loader-yoyo__cap" r="6.5" />
                  <path className="loader-yoyo__mark" d="M-8 0A8 8 0 0 1 0 -8" />
                  <circle className="loader-yoyo__axle" r="1.6" />
                </g>
              </g>
            </g>
            <rect className="loader-yoyo__finger" x="16" y="2" width="28" height="10" rx="5" />
            <path className="loader-yoyo__knuckle" d="M25 4.5Q24 7 25 9.5M35 4.5Q34 7 35 9.5" />
          </g>
        </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-yoyo
Source on GitHub

About this component

A yo-yo drops spinning off a finger, sleeps at the bottom of the string, then a tug sends it climbing back up into the hand.

Dependencies
None
Difficulty
Intermediate
Triggers
continuous, loop
Techniques
keyframes, svg, 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 is decorative. Under prefers-reduced-motion the yo-yo hangs still.

More loaders

See all
Ampersand
Beacon
Brew
Fireflies