<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="https://unpkg.com/react@15.7.0/dist/react.min.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@15.7.0/dist/react-dom.min.js" crossorigin></script>
  </body>
</html>
* {
  box-sizing: border-box;
}

body {
  margin: 0 auto;
  width: 80%;
  height: 80%;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
}

.root {
  display: inline-block;
}

.progressPath {
  stroke-dasharray: 60, 310;
  will-change: stroke, stroke-dashoffset;
}
function lerp(x, x0, x1, y0, y1) {
  const t = (x - x0) / (x1 - x0);
  return y0 + t * (y1 - y0);
}

function lerpColor(x, x0, x1, y0, y1) {
  const b0 = y0 & 0xff;
  const g0 = (y0 & 0xff00) >> 8;
  const r0 = (y0 & 0xff0000) >> 16;

  const b1 = y1 & 0xff;
  const g1 = (y1 & 0xff00) >> 8;
  const r1 = (y1 & 0xff0000) >> 16;

  const r = Math.floor(lerp(x, x0, x1, r0, r1));
  const g = Math.floor(lerp(x, x0, x1, g0, g1));
  const b = Math.floor(lerp(x, x0, x1, b0, b1));

  return "#" + ("00000" + ((r << 16) | (g << 8) | b).toString(16)).slice(-6);
}

function lerpTable(vIndex, tValue, table, canExtrapolate, lerpFunc = lerp) {
  const rowCount = table.length;

  for (let i = 0; i < rowCount; ++i) {
    let a = table[i][0];

    if (i === 0 && tValue < a) {
      if (canExtrapolate) {
        return lerpFunc(
          tValue,
          a,
          table[i + 1][0],
          table[i][vIndex],
          table[i + 1][vIndex]
        );
      }
      return table[i][vIndex];
    }

    if (i === rowCount - 1 && tValue >= a) {
      if (canExtrapolate) {
        return lerpFunc(
          tValue,
          table[i - 1][0],
          a,
          table[i - 1][vIndex],
          table[i][vIndex]
        );
      }
      return table[i][vIndex];
    }

    if (tValue >= a && tValue <= table[i + 1][0]) {
      return lerpFunc(
        tValue,
        a,
        table[i + 1][0],
        table[i][vIndex],
        table[i + 1][vIndex]
      );
    }
  }

  return 0;
}

function animate() {
  let pathWidth = 372;
  let speed = 2;
  const colorTable = [
    // ease in
    [0.0, 0xf15a31],
    [0.2, 0xffd31b],
    [0.33, 0xa6ce42],
    [0.4, 0x007ac1],
    [0.45, 0x007ac1],
    // ease out
    [0.55, 0x007ac1],
    [0.6, 0x007ac1],
    [0.67, 0xa6ce42],
    [0.8, 0xffd31b],
    [1.0, 0xf15a31]
  ];
  let currentAnim = Date.now();
  let offset = this.state.offset;
  let t = ((currentAnim - this._animStart) % 6000) / 6000; // we want 6secs for color animation cycle
  let colorValue = lerpTable(1, t, colorTable, false, lerpColor);
  offset -= speed;
  if (offset < 0) offset = pathWidth;
  this.setState({
    stroke: colorValue,
    offset: offset
  });
  this._animId =
    window.requestAnimationFrame &&
    window.requestAnimationFrame(animate.bind(this));

  return this._animId;
}

function start() {
  this._animStart = Date.now();
  this._animId = animate.call(this);
}

function stop() {
  if (this._animId) {
    window.cancelAnimationFrame && window.cancelAnimationFrame(this._animId);
    this._animId = null;
  }
}

class Spinner extends React.Component {
  constructor() {
    super(...arguments);

    this.state = {
      running: false,
      stroke: "#ededed",
      offset: 445
    };
  }

  componentDidMount() {
    start.call(this);
  }

  componentWillUnmount() {
    stop.call(this);
  }

  render() {
    let { offset, stroke } = this.state;
    let pathStyle = {
      stroke: stroke,
      strokeDashoffset: offset
    };

    return React.createElement(
      "svg",
      {
        height: "100vh",
        viewBox: "0 0 115 115",
        preserveAspectRatio: "xMidYMid meet",
        className: "root"
      },
      React.createElement("path", {
        opacity: "0.05",
        fill: "none",
        stroke: "#000000",
        strokeWidth: "3",
        d:
          "M 85 85 C -5 16 -39 127 78 30 C 126 -9 57 -16 85 85 C 94 123 124 111 85 85 Z"
      }),
      React.createElement("path", {
        style: pathStyle,
        className: "progressPath",
        fill: "none",
        strokeWidth: "3",
        strokeLinecap: "round",
        d:
          "M 85 85 C -5 16 -39 127 78 30 C 126 -9 57 -16 85 85 C 94 123 124 111 85 85 Z"
      })
    );
  }
}

ReactDOM.render(React.createElement(Spinner), document.getElementById("root"));
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.