Animated Counter Slider In React with Code Example
Learn how to create an animated counter slider in React. This tutorial covers how to use React hooks, framer-motion, and CSS animations to create a smooth and r
// Full open-source React component available via npm
// https://www.npmjs.com/package/react-animated-counter
import React, {
useState,
useEffect,
useRef
} from "https://cdn.skypack.dev/react@17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17.0.1";
import * as framerMotion from "https://cdn.skypack.dev/framer-motion@5.5.5";
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="1"
max="999"
onChange={(e) => setCounterValue(e.target.value)}
step="0.01"
style={getBackgroundSize(counterValue)}
value={counterValue}
/>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<body>
<div id="root"></div>
</body>
* {
margin: 0;
}
html, body {
height: 100%;
width: 100%;
}
#root {
height: 100%;
width: 100%;
background: radial-gradient(
ellipse farthest-corner at center top,
#012619,
#000
);
font-family: 'Comfortaa', cursive;
}
.App {
height: 100%;
width: 100%;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.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;
}