Ferris wheel
React + Tailwind
import type { CSSProperties } from 'react';
const css = `
@keyframes loader-ferris-turn { to { transform: rotate(360deg); } }
@keyframes loader-ferris-level { to { transform: rotate(-360deg); } }
@keyframes loader-ferris-swing { 0%, 100% { transform: rotate(-6deg); } 50% { transform: rotate(6deg); } }
@keyframes loader-ferris-bulb { 0%, 100% { opacity: 0.25; } 12% { opacity: 1; } 40% { opacity: 0.25; } }
.loader-ferris { position: relative; width: 124px; height: 120px; }
.loader-ferris svg { width: 100%; height: 100%; overflow: visible; }
.loader-ferris__leg { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 3.5; stroke-linecap: round; }
.loader-ferris__brace { stroke: light-dark(#cbced3, #393c41); stroke-width: 2; }
.loader-ferris__ground { stroke: light-dark(#6d7179, #7d8087); stroke-width: 3; stroke-linecap: round; }
.loader-ferris__wheel { transform-origin: 56px 46px; animation: loader-ferris-turn 9s linear infinite; }
.loader-ferris__rim { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 2; }
.loader-ferris__rim--in { stroke: light-dark(#cbced3, #393c41); stroke-width: 1.2; }
.loader-ferris__truss { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 1; stroke-linejoin: round; }
.loader-ferris__spoke { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.1; }
.loader-ferris__hub { fill: light-dark(#5f626a, #a7abb2); stroke: light-dark(#cbced3, #393c41); stroke-width: 1.5; }
.loader-ferris__bulb { fill: light-dark(#373a40, #f1f2f3); opacity: 0.25; animation: loader-ferris-bulb 1.6s linear infinite var(--d); }
/* Each cabin turns back against the wheel so it hangs level, and sways on its own hook. */
.loader-ferris__level { animation: loader-ferris-level 9s linear infinite; }
.loader-ferris__swing { animation: loader-ferris-swing 1.8s ease-in-out infinite var(--d); }
.loader-ferris__hook { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
.loader-ferris__cabin { fill: light-dark(#5f626a, #a7abb2); }
.loader-ferris__roof { fill: light-dark(#50535b, #c6c8cd); }
.loader-ferris__window { fill: light-dark(#ebedf0, #232529); }
@media (prefers-reduced-motion: reduce) {
.loader-ferris__wheel, .loader-ferris__level, .loader-ferris__swing, .loader-ferris__bulb { animation: none; }
.loader-ferris__bulb { opacity: 0.7; }
}
`;
const R = 36;
const rad = (a: number) => (a * Math.PI) / 180;
const at = (a: number, r: number) => [56 + r * Math.cos(rad(a)), 46 + r * Math.sin(rad(a))].map((v) => +v.toFixed(2));
const CABINS = Array.from({ length: 8 }, (_, i) => i * 45);
const BULBS = Array.from({ length: 16 }, (_, i) => i * 22.5 + 11.25);
// A zigzag of struts between the two rims.
const TRUSS = Array.from({ length: 33 }, (_, i) => at(i * 11.25, i % 2 ? R - 5 : R).join(' ')).join('L');
export function FerrisLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-ferris" role="status" aria-label="Loading">
<svg viewBox="0 0 112 108" aria-hidden="true">
<path className="loader-ferris__leg" d="M56 46L32 102M56 46L80 102" />
<path className="loader-ferris__brace" d="M41 82H71" />
<path className="loader-ferris__ground" d="M22 102H90" />
<g className="loader-ferris__wheel">
{CABINS.map((a) => (
<line key={a} className="loader-ferris__spoke" x1="56" y1="46" x2={at(a, R)[0]} y2={at(a, R)[1]} />
))}
<path className="loader-ferris__truss" d={`M${TRUSS}`} />
<circle className="loader-ferris__rim" cx="56" cy="46" r={R} />
<circle className="loader-ferris__rim loader-ferris__rim--in" cx="56" cy="46" r={R - 5} />
{BULBS.map((a, i) => (
<circle key={a} className="loader-ferris__bulb" cx={at(a, R)[0]} cy={at(a, R)[1]} r="1.3" style={{ '--d': `${(-i * 0.1).toFixed(1)}s` } as CSSProperties} />
))}
{CABINS.map((a, i) => (
<g key={a} transform={`translate(${at(a, R).join(' ')})`}>
<g className="loader-ferris__level">
<g className="loader-ferris__swing" style={{ '--d': `${(-i * 0.37).toFixed(2)}s` } as CSSProperties}>
<line className="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5" />
<path className="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z" />
<path className="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z" />
<rect className="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1" />
</g>
</g>
</g>
))}
<circle className="loader-ferris__hub" cx="56" cy="46" r="4" />
</g>
</svg>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-ferris" role="status" aria-label="Loading">
<svg viewBox="0 0 112 108" aria-hidden="true">
<path class="loader-ferris__leg" d="M56 46L32 102M56 46L80 102"></path>
<path class="loader-ferris__brace" d="M41 82H71"></path>
<path class="loader-ferris__ground" d="M22 102H90"></path>
<g class="loader-ferris__wheel">
<line class="loader-ferris__spoke" x1="56" y1="46" x2="92" y2="46"></line>
<line class="loader-ferris__spoke" x1="56" y1="46" x2="81.46" y2="71.46"></line>
<line class="loader-ferris__spoke" x1="56" y1="46" x2="56" y2="82"></line>
<line class="loader-ferris__spoke" x1="56" y1="46" x2="30.54" y2="71.46"></line>
<line class="loader-ferris__spoke" x1="56" y1="46" x2="20" y2="46"></line>
<line class="loader-ferris__spoke" x1="56" y1="46" x2="30.54" y2="20.54"></line>
<line class="loader-ferris__spoke" x1="56" y1="46" x2="56" y2="10"></line>
<line class="loader-ferris__spoke" x1="56" y1="46" x2="81.46" y2="20.54"></line>
<path class="loader-ferris__truss" d="M92 46L86.4 52.05L89.26 59.78L81.78 63.22L81.46 71.46L73.22 71.78L69.78 79.26L62.05 76.4L56 82L49.95 76.4L42.22 79.26L38.78 71.78L30.54 71.46L30.22 63.22L22.74 59.78L25.6 52.05L20 46L25.6 39.95L22.74 32.22L30.22 28.78L30.54 20.54L38.78 20.22L42.22 12.74L49.95 15.6L56 10L62.05 15.6L69.78 12.74L73.22 20.22L81.46 20.54L81.78 28.78L89.26 32.22L86.4 39.95L92 46"></path>
<circle class="loader-ferris__rim" cx="56" cy="46" r="36"></circle>
<circle class="loader-ferris__rim loader-ferris__rim--in" cx="56" cy="46" r="31"></circle>
<circle class="loader-ferris__bulb" cx="91.31" cy="53.02" r="1.3" style="--d:0.0s"></circle>
<circle class="loader-ferris__bulb" cx="85.93" cy="66" r="1.3" style="--d:-0.1s"></circle>
<circle class="loader-ferris__bulb" cx="76" cy="75.93" r="1.3" style="--d:-0.2s"></circle>
<circle class="loader-ferris__bulb" cx="63.02" cy="81.31" r="1.3" style="--d:-0.3s"></circle>
<circle class="loader-ferris__bulb" cx="48.98" cy="81.31" r="1.3" style="--d:-0.4s"></circle>
<circle class="loader-ferris__bulb" cx="36" cy="75.93" r="1.3" style="--d:-0.5s"></circle>
<circle class="loader-ferris__bulb" cx="26.07" cy="66" r="1.3" style="--d:-0.6s"></circle>
<circle class="loader-ferris__bulb" cx="20.69" cy="53.02" r="1.3" style="--d:-0.7s"></circle>
<circle class="loader-ferris__bulb" cx="20.69" cy="38.98" r="1.3" style="--d:-0.8s"></circle>
<circle class="loader-ferris__bulb" cx="26.07" cy="26" r="1.3" style="--d:-0.9s"></circle>
<circle class="loader-ferris__bulb" cx="36" cy="16.07" r="1.3" style="--d:-1.0s"></circle>
<circle class="loader-ferris__bulb" cx="48.98" cy="10.69" r="1.3" style="--d:-1.1s"></circle>
<circle class="loader-ferris__bulb" cx="63.02" cy="10.69" r="1.3" style="--d:-1.2s"></circle>
<circle class="loader-ferris__bulb" cx="76" cy="16.07" r="1.3" style="--d:-1.3s"></circle>
<circle class="loader-ferris__bulb" cx="85.93" cy="26" r="1.3" style="--d:-1.4s"></circle>
<circle class="loader-ferris__bulb" cx="91.31" cy="38.98" r="1.3" style="--d:-1.5s"></circle>
<g transform="translate(92 46)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:0.00s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<g transform="translate(81.46 71.46)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:-0.37s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<g transform="translate(56 82)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:-0.74s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<g transform="translate(30.54 71.46)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:-1.11s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<g transform="translate(20 46)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:-1.48s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<g transform="translate(30.54 20.54)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:-1.85s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<g transform="translate(56 10)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:-2.22s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<g transform="translate(81.46 20.54)">
<g class="loader-ferris__level">
<g class="loader-ferris__swing" style="--d:-2.59s">
<line class="loader-ferris__hook" x1="0" y1="0" x2="0" y2="3.5"></line>
<path class="loader-ferris__cabin" d="M-5.5 4H5.5L5 12Q5 13.5 3.5 13.5H-3.5Q-5 13.5 -5 12Z"></path>
<path class="loader-ferris__roof" d="M-7 5Q0 0.5 7 5Z"></path>
<rect class="loader-ferris__window" x="-3.5" y="6.5" width="7" height="3" rx="1"></rect>
</g>
</g>
</g>
<circle class="loader-ferris__hub" cx="56" cy="46" r="4"></circle>
</g>
</svg>
</div>CSS
@keyframes loader-ferris-turn { to { transform: rotate(360deg); } }
@keyframes loader-ferris-level { to { transform: rotate(-360deg); } }
@keyframes loader-ferris-swing { 0%, 100% { transform: rotate(-6deg); } 50% { transform: rotate(6deg); } }
@keyframes loader-ferris-bulb { 0%, 100% { opacity: 0.25; } 12% { opacity: 1; } 40% { opacity: 0.25; } }
.loader-ferris { position: relative; width: 124px; height: 120px; }
.loader-ferris svg { width: 100%; height: 100%; overflow: visible; }
.loader-ferris__leg { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 3.5; stroke-linecap: round; }
.loader-ferris__brace { stroke: light-dark(#cbced3, #393c41); stroke-width: 2; }
.loader-ferris__ground { stroke: light-dark(#6d7179, #7d8087); stroke-width: 3; stroke-linecap: round; }
.loader-ferris__wheel { transform-origin: 56px 46px; animation: loader-ferris-turn 9s linear infinite; }
.loader-ferris__rim { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 2; }
.loader-ferris__rim--in { stroke: light-dark(#cbced3, #393c41); stroke-width: 1.2; }
.loader-ferris__truss { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 1; stroke-linejoin: round; }
.loader-ferris__spoke { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.1; }
.loader-ferris__hub { fill: light-dark(#5f626a, #a7abb2); stroke: light-dark(#cbced3, #393c41); stroke-width: 1.5; }
.loader-ferris__bulb { fill: light-dark(#373a40, #f1f2f3); opacity: 0.25; animation: loader-ferris-bulb 1.6s linear infinite var(--d); }
/* Each cabin turns back against the wheel so it hangs level, and sways on its own hook. */
.loader-ferris__level { animation: loader-ferris-level 9s linear infinite; }
.loader-ferris__swing { animation: loader-ferris-swing 1.8s ease-in-out infinite var(--d); }
.loader-ferris__hook { stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
.loader-ferris__cabin { fill: light-dark(#5f626a, #a7abb2); }
.loader-ferris__roof { fill: light-dark(#50535b, #c6c8cd); }
.loader-ferris__window { fill: light-dark(#ebedf0, #232529); }
@media (prefers-reduced-motion: reduce) {
.loader-ferris__wheel, .loader-ferris__level, .loader-ferris__swing, .loader-ferris__bulb { animation: none; }
.loader-ferris__bulb { opacity: 0.7; }
}Goal: Create a "Ferris wheel" loader.
Visual behavior: A ferris wheel turning slowly with chase lights running round the rim, its cabins hanging level and swinging gently as they go.
Technique:
- The wheel turns once every nine seconds. Each cabin hangs from its spoke end inside a group that turns back by the same amount, so it stays level, and a swing keyframe with its own offset sways it on its hook.
- Sixteen bulbs on the rim brighten in turn with staggered delays, so the light runs round.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the wheel is still and the lights stay on.
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 "Ferris wheel" loader.
Visual behavior: A ferris wheel turning slowly with chase lights running round the rim, its cabins hanging level and swinging gently as they go.
Technique:
- The wheel turns once every nine seconds. Each cabin hangs from its spoke end inside a group that turns back by the same amount, so it stays level, and a swing keyframe with its own offset sways it on its hook.
- Sixteen bulbs on the rim brighten in turn with staggered delays, so the light runs round.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the wheel is still and the lights stay on.
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 ferris wheel turning slowly with chase lights running round the rim, its cabins hanging level and swinging gently as they go.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, glow, delay stagger
- Techniques
- keyframes, stagger, svg
- 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 wheel is still and the lights stay on.