Count-up stats
Numbers roll up from zero with an ease-out curve the moment they come into view.
Bars of a chart grow to their values in sequence when the chart scrolls into view.
Bars start at scaleY(0) with transform-origin at the bottom. A class added by an IntersectionObserver sets scaleY(1), and each bar has a slightly longer transition delay than the last.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Growing bar chart</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: #f1f5f9; color: #0f172a; }
.pad { height: 70vh; display: grid; place-content: center; }
.pad h1 { font-size: 2.6rem; margin: 0; }
.chart { max-width: 760px; margin: 0 auto; padding: 0 24px; }
.chart figcaption { font: 600 1.1rem system-ui, sans-serif; margin-bottom: 20px; }
.bars { height: 50vh; display: flex; align-items: flex-end; gap: 2%; border-bottom: 2px solid #0f172a; }
.bar { flex: 1; height: var(--h); background: #6366f1; border-radius: 6px 6px 0 0; position: relative;
transform: scaleY(0); transform-origin: bottom; transition: transform .9s cubic-bezier(.2,.8,.2,1); transition-delay: calc(var(--i) * 60ms); }
.bar:nth-child(3n) { background: #14b8a6; }
.bar span { position: absolute; bottom: -1.8em; left: 0; right: 0; text-align: center; font-size: .85rem; }
.chart.in .bar { transform: scaleY(1); }
</style>
</head>
<body>
<div class="pad"><h1>Growing bar chart</h1></div>
<figure class="chart" id="chart"><figcaption>Average daily scroll distance, metres</figcaption><div class="bars" id="bars"></div></figure>
<div class="pad"></div>
<script>
const data = [['Mon', 62], ['Tue', 74], ['Wed', 58], ['Thu', 81], ['Fri', 95], ['Sat', 70], ['Sun', 88]];
const bars = document.getElementById('bars');
data.forEach(([d, v], i) => bars.insertAdjacentHTML('beforeend', '<div class="bar" style="--h:' + v + '%;--i:' + i + '"><span>' + d + '</span></div>'));
const chart = document.getElementById('chart');
new IntersectionObserver(([e]) => chart.classList.toggle('in', e.isIntersecting), { threshold: 0.4 }).observe(chart);
</script>
</body>
</html>