Yo-yo
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>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-yoyo" role="status" aria-label="Loading">
<svg viewBox="0 0 60 100" aria-hidden="true">
<ellipse class="loader-yoyo__shadow" cx="30" cy="96" rx="12" ry="2"></ellipse>
<g class="loader-yoyo__hand">
<rect class="loader-yoyo__string" x="29.4" y="10" width="1.2" height="68"></rect>
<g class="loader-yoyo__drop">
<g transform="translate(30 26)">
<g class="loader-yoyo__spin">
<circle class="loader-yoyo__shell" r="11"></circle>
<circle class="loader-yoyo__cap" r="6.5"></circle>
<path class="loader-yoyo__mark" d="M-8 0A8 8 0 0 1 0 -8"></path>
<circle class="loader-yoyo__axle" r="1.6"></circle>
</g>
</g>
</g>
<rect class="loader-yoyo__finger" x="16" y="2" width="28" height="10" rx="5"></rect>
<path class="loader-yoyo__knuckle" d="M25 4.5Q24 7 25 9.5M35 4.5Q34 7 35 9.5"></path>
</g>
</svg>
</div>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; }
}Goal: Create a "Yo-yo" loader.
Visual behavior: 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.
Technique:
- The drop and the string share keyframe stops and timing functions: the yo-yo translates down while the string scales from the finger by the matching amount, so the string always ends at the axle.
- The spin speeds up on the way down, runs fast while it sleeps and slows as it climbs, ending on whole turns so the loop is seamless. A tug lifts the finger just before it climbs, and a floor shadow grows as it nears.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the yo-yo hangs still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Yo-yo" loader.
Visual behavior: 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.
Technique:
- The drop and the string share keyframe stops and timing functions: the yo-yo translates down while the string scales from the finger by the matching amount, so the string always ends at the axle.
- The spin speeds up on the way down, runs fast while it sleeps and slows as it climbs, ending on whole turns so the loop is seamless. A tug lifts the finger just before it climbs, and a floor shadow grows as it nears.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the yo-yo hangs still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
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.