← Illustrations

Ferris Wheel

A Ferris wheel turns slowly on its stand. Its cabins hang from the rim and stay upright as they go round, each swinging gently on its pin, while lights chase round the rim and clouds drift past behind.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-ferris-wheel> where the scene should go. It fills the element with an SVG scene.

Options

Add any of these attributes to the same element.

data-colorColour of the frame. Default #7c3aed.
data-cabinsNumber of cabins, 4 to 16. Default 10.
data-colorsComma-separated cabin colours, used in turn. Default pink, yellow, blue, green and orange.
data-speedSpeed multiplier. At 1 the wheel turns once every 24 seconds. Default 1.
data-lights="false"No lights on the rim.

It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion the wheel stands still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Ferris Wheel: a Ferris wheel turns slowly on its stand. The cabins hang
    // from the rim and stay upright as they go round, swinging gently on their
    // pins, while lights chase round the rim and clouds drift past.
    // Drawn as SVG that fills the element, and paused when off screen.
    // Usage: <div data-ferris-wheel></div>
    // It is 8:5 by default on a transparent background.
    // Optional: data-color (colour of the frame, default #7c3aed),
    // data-cabins (number of cabins, 4 to 16, default 10), data-colors
    // (comma-separated cabin colours), data-speed (default 1: one turn every
    // 24 s), data-lights="false" (no rim lights).
    var DEFAULTS = {
      color: '#7c3aed',
      cabins: 10,
      speed: 1,
      colors: ['#f472b6', '#facc15', '#38bdf8', '#4ade80', '#fb923c']
    };
    var NS = 'http://www.w3.org/2000/svg';
    var CX = 200, CY = 108, R = 84;
    var TURN = 24000;
    var LIGHTS = 32;

    var css =
      '[data-ferris-wheel]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
      '.fwh-svg{position:absolute;inset:0;width:100%;height:100%;display:block}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function shade(hex, amt) {
      var h = hex.replace('#', '');
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      var out = '#';
      for (var i = 0; i < 3; i++) {
        var c = parseInt(h.substr(i * 2, 2), 16);
        c = amt < 0 ? c * (1 + amt) : c + (255 - c) * amt;
        out += ('0' + Math.round(c).toString(16)).slice(-2);
      }
      return out;
    }

    function cloud(x, y, s) {
      return '<g class="fwh-cloud" data-x="' + x + '" data-y="' + y + '" data-s="' + s + '" fill="#e2e8f0" opacity=".8">' +
        '<ellipse cx="0" cy="0" rx="' + 22 * s + '" ry="' + 8 * s + '"/>' +
        '<circle cx="' + -6 * s + '" cy="' + -6 * s + '" r="' + 9 * s + '"/>' +
        '<circle cx="' + 7 * s + '" cy="' + -8 * s + '" r="' + 11 * s + '"/></g>';
    }

    function build(el) {
      var d = el.dataset;
      var frameCol = d.color || DEFAULTS.color;
      var n = Math.max(4, Math.min(16, parseInt(d.cabins, 10) || DEFAULTS.cabins));
      var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }).filter(Boolean) : DEFAULTS.colors;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var lightsOn = d.lights !== 'false';
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A Ferris wheel turning with its cabins swinging');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      var dark = shade(frameCol, -0.3), light = shade(frameCol, 0.35);
      // The wheel: two rims, spokes in pairs and a hub.
      var spokes = '';
      for (var i = 0; i < n; i++) {
        var a = i / n * Math.PI * 2;
        var x = CX + Math.cos(a) * R, y = CY + Math.sin(a) * R;
        spokes += '<path d="M' + CX + ' ' + CY + 'L' + x.toFixed(1) + ' ' + y.toFixed(1) + '"/>';
        var a2 = (i + 0.5) / n * Math.PI * 2;
        // Cross-bracing between the rims.
        spokes += '<path d="M' + (CX + Math.cos(a) * R * 0.62).toFixed(1) + ' ' + (CY + Math.sin(a) * R * 0.62).toFixed(1) + 'L' + (CX + Math.cos(a2) * R).toFixed(1) + ' ' + (CY + Math.sin(a2) * R).toFixed(1) + '" stroke-opacity=".55" stroke-width="1.2"/>';
      }
      var bulbs = '';
      if (lightsOn) {
        for (var b = 0; b < LIGHTS; b++) {
          var ba = b / LIGHTS * Math.PI * 2;
          bulbs += '<circle class="fwh-bulb" cx="' + (CX + Math.cos(ba) * R).toFixed(1) + '" cy="' + (CY + Math.sin(ba) * R).toFixed(1) + '" r="1.9" fill="#fde68a"/>';
        }
      }

      el.innerHTML =
        '<svg class="fwh-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        cloud(70, 48, 1) + cloud(320, 30, 0.8) + cloud(350, 92, 0.6) +
        // Ground and the stand behind the wheel.
        '<ellipse cx="200" cy="238" rx="150" ry="6" fill="#1e1b2e" opacity=".07"/>' +
        '<path d="M' + CX + ' ' + CY + 'L150 236M' + CX + ' ' + CY + 'L250 236" stroke="' + dark + '" stroke-width="5" stroke-linecap="round"/>' +
        '<g class="fwh-wheel" fill="none" stroke="' + frameCol + '" stroke-linecap="round">' +
          '<circle cx="' + CX + '" cy="' + CY + '" r="' + R + '" stroke-width="4"/>' +
          '<circle cx="' + CX + '" cy="' + CY + '" r="' + (R * 0.62).toFixed(1) + '" stroke-width="2.4" stroke="' + light + '"/>' +
          '<g stroke-width="1.8">' + spokes + '</g>' +
          bulbs +
        '</g>' +
        '<g class="fwh-cabins"></g>' +
        // Front leg pair and hub sit over the wheel.
        '<path d="M' + CX + ' ' + CY + 'L164 236M' + CX + ' ' + CY + 'L236 236" stroke="' + frameCol + '" stroke-width="5" stroke-linecap="round"/>' +
        '<path d="M168 214H232" stroke="' + frameCol + '" stroke-width="3" stroke-linecap="round"/>' +
        '<circle cx="' + CX + '" cy="' + CY + '" r="9" fill="' + dark + '"/><circle cx="' + CX + '" cy="' + CY + '" r="4" fill="' + light + '"/>' +
        // Boarding platform.
        '<rect x="136" y="232" width="128" height="8" rx="3" fill="' + dark + '"/>' +
        '<rect x="182" y="222" width="36" height="12" rx="2" fill="' + light + '"/><rect x="190" y="225" width="8" height="9" fill="' + dark + '" opacity=".6"/><rect x="202" y="225" width="10" height="5" rx="1" fill="#fff" opacity=".8"/>' +
        '</svg>';

      var svg = el.firstChild;
      var wheel = svg.querySelector('.fwh-wheel');
      var cabG = svg.querySelector('.fwh-cabins');
      var bulbEls = svg.querySelectorAll('.fwh-bulb');
      var clouds = svg.querySelectorAll('.fwh-cloud');
      var cabins = [];
      for (var c = 0; c < n; c++) {
        var col = colors[c % colors.length];
        var g = document.createElementNS(NS, 'g');
        g.innerHTML =
          '<path d="M0 0V8" stroke="' + dark + '" stroke-width="2" stroke-linecap="round"/>' +
          '<path d="M-13 12Q0 2 13 12Z" fill="' + shade(col, -0.25) + '"/>' +
          '<rect x="-11" y="11" width="22" height="17" rx="4" fill="' + col + '"/>' +
          '<rect x="-7.5" y="14" width="15" height="7" rx="2" fill="#fff" opacity=".75"/>' +
          '<path d="M0 14V21" stroke="' + col + '" stroke-width="1.5"/>' +
          '<rect x="-11" y="24" width="22" height="4" rx="2" fill="' + shade(col, -0.2) + '"/>' +
          '<circle cx="0" cy="0" r="2.2" fill="' + light + '" stroke="' + dark + '" stroke-width="1"/>';
        cabG.appendChild(g);
        cabins.push({ g: g, phase: Math.random() * Math.PI * 2, amp: 4 + Math.random() * 4 });
      }

      function frame(t) {
        var s = t / 1000;
        var turn = (t / TURN) * Math.PI * 2;
        wheel.setAttribute('transform', 'rotate(' + (turn * 180 / Math.PI).toFixed(2) + ' ' + CX + ' ' + CY + ')');
        cabins.forEach(function (cb, i) {
          var a = turn + i / n * Math.PI * 2;
          var x = CX + Math.cos(a) * R, y = CY + Math.sin(a) * R;
          // Each cabin swings on its pin; the swing builds a little as it rounds the top.
          var swing = cb.amp * Math.sin(s * 1.9 + cb.phase) * (0.7 + 0.3 * Math.cos(a + Math.PI / 2)) + 5 * Math.cos(a) * 0.4;
          cb.g.setAttribute('transform', 'translate(' + x.toFixed(1) + ' ' + y.toFixed(1) + ') rotate(' + swing.toFixed(2) + ')');
        });
        // Lights chase round the rim in groups.
        for (var k = 0; k < bulbEls.length; k++) {
          var on = 0.5 + 0.5 * Math.sin(k * 0.8 - s * 4);
          bulbEls[k].setAttribute('opacity', (0.25 + 0.75 * on * on).toFixed(2));
        }
        for (var m = 0; m < clouds.length; m++) {
          var cl = clouds[m];
          var cx = ((+cl.getAttribute('data-x') + s * (3 + m * 1.5)) % 480 + 480) % 480 - 40;
          cl.setAttribute('transform', 'translate(' + cx.toFixed(1) + ' ' + cl.getAttribute('data-y') + ')');
        }
      }

      if (reduced()) { frame(0); return; }

      var t = 0, last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          t += (last ? Math.min(50, now - last) : 16) * speed;
          last = now;
          frame(t);
        }
      });
      frame(0);
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-ferris-wheel]').forEach(function (el) {
        if (el._fwh) return;   // idempotent
        el._fwh = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-ferris-wheel></div>

<!-- a smaller, faster wheel in brand colours: -->
<div data-ferris-wheel data-cabins="6" data-speed="2" data-colors="#7c3aed,#ec4899"></div>