Animated Counter Slider In React

Description:

An interactive animated counter slider built with React and framer-motion.

How to use it:

1. Import the necessary libraries.

import React, {
  useState,
  useEffect,
  useRef
} from "https://cdn.skypack.dev/[email protected]";
import ReactDOM from "https://cdn.skypack.dev/[email protected]";
import * as framerMotion from "https://cdn.skypack.dev/[email protected]";

2. The main script.

const { motion } = framerMotion;
const usePrevious = (value) => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
};
const getBackgroundSize = (value) => {
  return { backgroundSize: `${(value * 100) / 1000}% 100%` };
};
const formatForDisplay = (number, includeDecimals) => {
  return parseFloat(Math.max(number, 0))
    .toFixed(includeDecimals ? 2 : 0)
    .split("")
    .reverse();
};
const DecimalColumn = ({ fontSize, color }) => {
  return (
    <div>
      <span
        style={{
          fontSize: fontSize,
          lineHeight: fontSize,
          color: color
        }}
      >
        .
      </span>
    </div>
  );
};
const NumberColumn = ({
  digit,
  delta,
  fontSize,
  color,
  incrementColor,
  decrementColor
}) => {
  const [position, setPosition] = useState(0);
  const [animationClass, setAnimationClass] = useState(null);
  const previousDigit = usePrevious(digit);
  const columnContainer = useRef();
  const setColumnToNumber = (number) => {
    setPosition(columnContainer.current.clientHeight * parseInt(number, 10));
  };
  useEffect(() => setAnimationClass(previousDigit !== digit ? delta : ""), [
    digit,
    delta
  ]);
  useEffect(() => setColumnToNumber(digit), [digit]);
  return (
    <div
      className="ticker-column-container"
      ref={columnContainer}
      style={{
        fontSize: fontSize,
        lineHeight: fontSize,
        color: color,
        height: "auto",
        "--increment-color": incrementColor,
        "--decrement-color": decrementColor
      }}
    >
      <motion.div
        animate={{ x: 0, y: position }}
        className={`ticker-column ${animationClass}`}
        onAnimationComplete={() => setAnimationClass("")}
      >
        {[9, 8, 7, 6, 5, 4, 3, 2, 1, 0].map((num) => (
          <div key={num} className="ticker-digit">
            <span
              style={{
                fontSize: fontSize,
                lineHeight: fontSize,
                color: color,
              }}
            >
              {num}
            </span>
          </div>
        ))}
      </motion.div>
      <span className="number-placeholder">0</span>
    </div>
  );
};
// Counter component
const AnimatedCounter = ({
  value = 0,
  fontSize = "18px",
  color = "white",
  incrementColor = "#32cd32",
  decrementColor = "#fe6862",
  includeDecimals = true
}) => {
  const numArray = formatForDisplay(value, includeDecimals);
  const previousNumber = usePrevious(value);
  let delta = null;
  if (value > previousNumber) delta = "increase";
  if (value < previousNumber) delta = "decrease";
  return (
    <motion.div layout className="ticker-view">
      {numArray.map((number, index) =>
        number === "." ? (
          <DecimalColumn key={index} fontSize={fontSize} color={color} />
        ) : (
          <NumberColumn
            key={index}
            digit={number}
            delta={delta}
            fontSize={fontSize}
            incrementColor={incrementColor}
            decrementColor={decrementColor}
            includeDecimals={includeDecimals}
          />
        )
      )}
    </motion.div>
  );
};
// Main app component
const App = () => {
  const [counterValue, setCounterValue] = useState(500.00);
  return (
    <div className="App">
      <AnimatedCounter value={counterValue} fontSize='48px'/>
      <input
        type="range"
        min="0"
        max="1000"
        onChange={(e) => setCounterValue(e.target.value)}
        step="0.01"
        style={getBackgroundSize(counterValue)}
        value={counterValue}
      />
    </div>
  );
};
ReactDOM.render(<App />, document.getElementById("root"));

3. Necessary CSS styles.

.ticker-view {
  height: auto;
  display: flex;
  flex-direction: row-reverse;
  overflow: hidden;
  position: relative;
}
.number-placeholder {
  visibility: hidden;
}
.ticker-column-container {
  position: relative;
}
.ticker-column {
  position: absolute;
  height: 1000%;
  bottom: 0;
}
.ticker-digit {
  width: auto;
  height: 10%;
}
.ticker-column.increase {
  animation: pulseGreen 500ms cubic-bezier(0.4, 0, 0.6, 1) 1;
}
.ticker-column.decrease {
  animation: pulseRed 500ms cubic-bezier(0.4, 0, 0.6, 1) 1;
}
@keyframes pulseGreen {
  0%,
  100% {
    color: inherit;
  }
  50% {
    color: var(--increment-color);;
  }
}
@keyframes pulseRed {
  0%,
  100% {
    color: inherit;
  }
  50% {
    color: var(--decrement-color);
  }
}
input[type="range"] {
  margin-top: 24px;
  width: 250px;
  -webkit-appearance: none;
  height: 7px;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 5px;
  background-image: linear-gradient(#73d46a, #73d46a);
  background-repeat: no-repeat;
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  height: 20px;
  width: 20px;
  border-radius: 50%;
  background: #73d46a;
  cursor: pointer;
  box-shadow: 0 0 2px 0 #555;
  transition: background 0.3s ease-in-out;
}
input[type="range"]::-webkit-slider-runnable-track {
  -webkit-appearance: none;
  box-shadow: none;
  border: none;
  background: transparent;
}
input[type="range"]::-webkit-slider-thumb:hover {
  box-shadow: #73d46a50 0px 0px 0px 8px;
}
input[type="range"]::-webkit-slider-thumb:active {
  box-shadow: #73d46a50 0px 0px 0px 11px;
  transition: box-shadow 350ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
    left 350ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
    bottom 350ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
}

Preview:

Animated Counter Slider In React

Add Comment