15 CSS Number Counter Animations
Reading Progress Counter
Warm parchment card with a thin red progress bar across the top — like a real reading indicator. Lora serif counts up words read, with pages, hours, and minutes remaining below. Twenty chapter dots show exact position. Built for e-reader apps and Substack-style blogs.
Reading Progress Counter the 7th of 15 designs in the 15 CSS Number Counter Animations collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
The code
<div class="cnc-read">
<div class="cnc-read-card">
<div class="cnc-read-progress-bar"><div class="cnc-read-progress-fill"></div></div>
<div class="cnc-read-body">
<div class="cnc-read-chapter">Chapter 14 · The Inheritance</div>
<div class="cnc-read-count-row">
<span class="cnc-read-big" data-words data-target="74320">0</span>
<span class="cnc-read-count-unit">words</span>
</div>
<div class="cnc-read-sublabel">read so far · <span data-pct data-target="73">0</span>% of this book</div>
<hr class="cnc-read-rule">
<div class="cnc-read-stats">
<div class="cnc-read-stat"><div class="cnc-read-stat-n" data-pages data-target="248">0</div><div class="cnc-read-stat-l">Pages</div></div>
<div class="cnc-read-stat"><div class="cnc-read-stat-n" data-hrs data-target="4.9">0</div><div class="cnc-read-stat-l">Hours</div></div>
<div class="cnc-read-stat"><div class="cnc-read-stat-n" data-left data-target="162">0</div><div class="cnc-read-stat-l">Min left</div></div>
</div>
<div class="cnc-read-dots">
<div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div>
<div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div>
<div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div>
<div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div>
<div class="cnc-read-dot cnc-read-dot-read"></div><div class="cnc-read-dot cnc-read-dot-read"></div>
<div class="cnc-read-dot cnc-read-dot-current"></div>
<div class="cnc-read-dot"></div><div class="cnc-read-dot"></div><div class="cnc-read-dot"></div>
<div class="cnc-read-dot"></div><div class="cnc-read-dot"></div>
</div>
</div>
</div>
</div> .cnc-read { display: grid; place-items: center; padding: 32px 16px; background: #f0ebe3; font-family: 'Karla', sans-serif; }
.cnc-read *, .cnc-read *::before, .cnc-read *::after { box-sizing: border-box; }
.cnc-read-card { width: 340px; background: #faf8f4; border-radius: 4px; box-shadow: 0 2px 0 #d6cfc4, 0 12px 48px rgba(0,0,0,0.06); overflow: hidden; position: relative; }
.cnc-read-progress-bar { height: 3px; background: #e8e0d4; position: relative; }
.cnc-read-progress-fill { height: 100%; background: #c1440e; transform: scaleX(0); transform-origin: left; animation: cnc-read-progFill 2.4s cubic-bezier(0.22,1,0.36,1) 0.5s forwards; width: 73%; }
@keyframes cnc-read-progFill { to { transform: scaleX(1); } }
.cnc-read-body { padding: 40px 36px 36px; }
.cnc-read-chapter { font-size: 9px; letter-spacing: 4px; text-transform: uppercase; color: #b8a898; margin-bottom: 20px; font-weight: 400; opacity: 0; animation: cnc-read-fadeUp 0.5s ease 0.1s forwards; }
.cnc-read-count-row { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; opacity: 0; animation: cnc-read-fadeUp 0.7s cubic-bezier(0.22,1,0.36,1) 0.2s forwards; }
.cnc-read-big { font-family: 'Lora', serif; font-size: 80px; font-weight: 600; color: #1a1410; line-height: 1; letter-spacing: -3px; }
.cnc-read-count-unit { font-family: 'Lora', serif; font-style: italic; font-size: 18px; color: #a09080; font-weight: 400; letter-spacing: 0; padding-bottom: 6px; }
.cnc-read-sublabel { font-size: 11px; color: #b8a898; font-weight: 300; letter-spacing: 0.3px; margin-bottom: 32px; opacity: 0; animation: cnc-read-fadeUp 0.5s ease 0.3s forwards; }
.cnc-read-rule { border: none; border-top: 1px solid #ede7de; margin-bottom: 24px; opacity: 0; animation: cnc-read-fadeUp 0.4s ease 0.35s forwards; }
.cnc-read-stats { display: flex; gap: 0; opacity: 0; animation: cnc-read-fadeUp 0.5s ease 0.4s forwards; }
.cnc-read-stat { flex: 1; border-right: 1px solid #ede7de; padding-right: 16px; margin-right: 16px; }
.cnc-read-stat:last-child { border-right: none; padding-right: 0; margin-right: 0; }
.cnc-read-stat-n { font-family: 'Lora', serif; font-size: 24px; font-weight: 600; color: #1a1410; letter-spacing: -0.5px; line-height: 1; }
.cnc-read-stat-l { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; color: #b8a898; margin-top: 5px; font-weight: 300; }
.cnc-read-dots { display: flex; gap: 4px; margin-top: 28px; opacity: 0; animation: cnc-read-fadeUp 0.4s ease 0.5s forwards; flex-wrap: wrap; }
.cnc-read-dot { width: 6px; height: 6px; border-radius: 50%; background: #e0d8ce; }
.cnc-read-dot-read { background: #c1440e; }
.cnc-read-dot-current { background: #c1440e; box-shadow: 0 0 0 2px #faf8f4, 0 0 0 3px #c1440e; }
@keyframes cnc-read-fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
.cnc-read-progress-fill, .cnc-read-chapter, .cnc-read-count-row, .cnc-read-sublabel, .cnc-read-rule, .cnc-read-stats, .cnc-read-dots { animation: none; opacity: 1; transform: none; }
.cnc-read-progress-fill { transform: scaleX(0.73); }
} (function () {
var root = document.querySelector('.cnc-read');
if (!root) return;
function ease(t) { return 1 - Math.pow(1 - t, 3); }
setTimeout(function () {
var start = performance.now(), dur = 2000;
var wordsEl = root.querySelector('[data-words]');
var pctEl = root.querySelector('[data-pct]');
var pagesEl = root.querySelector('[data-pages]');
var hrsEl = root.querySelector('[data-hrs]');
var leftEl = root.querySelector('[data-left]');
var wordsTarget = parseFloat(wordsEl.dataset.target);
var pctTarget = parseFloat(pctEl.dataset.target);
var pagesTarget = parseFloat(pagesEl.dataset.target);
var hrsTarget = parseFloat(hrsEl.dataset.target);
var leftTarget = parseFloat(leftEl.dataset.target);
function tick(now) {
var t = Math.min((now - start) / dur, 1), e = ease(t);
wordsEl.textContent = Math.round(e * wordsTarget).toLocaleString();
pctEl.textContent = Math.round(e * pctTarget);
pagesEl.textContent = Math.round(e * pagesTarget);
hrsEl.textContent = (e * hrsTarget).toFixed(1);
leftEl.textContent = Math.round((1 - e) * leftTarget);
if (t < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}, 400);
})();