Skip to content

Count-Up Price Reveal

A copy-paste pricing tables component in pure HTML, CSS & vanilla JS. Zero dependencies, framework-agnostic, MIT-licensed.

Pricing TablesHTMLJavaScriptCSSany framework
Starting at
49/mo
Billed annually

Copy into your project

HTML
<!-- Count-Up Price Reveal — fades in on scroll, number counts up to target -->
<div class="nuda-pr2-count-up">
  <span class="nuda-pr2-count-up__label">Starting at</span>
  <div class="nuda-pr2-count-up__price">
    <b class="nuda-pr2-count-up__num" data-target="49">0</b><i>/mo</i>
  </div>
  <span class="nuda-pr2-count-up__sub">Billed annually</span>
</div>
JavaScript
/* Count-Up Price Reveal — reveals + counts up once scrolled into view. */

(function () {
  var card = document.querySelector('.nuda-pr2-count-up');
  if (!card) return;
  var numEl = card.querySelector('.nuda-pr2-count-up__num');
  var target = Number(numEl.dataset.target || numEl.textContent);
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  function run() {
    card.classList.add('is-visible');
    if (reduce) {
      numEl.textContent = target;
      card.classList.add('is-done');
      return;
    }
    var start = null;
    var duration = 900;
    function step(ts) {
      if (start === null) start = ts;
      var progress = Math.min(1, (ts - start) / duration);
      numEl.textContent = Math.round(progress * target);
      if (progress < 1) {
        requestAnimationFrame(step);
      } else {
        card.classList.add('is-done');
      }
    }
    requestAnimationFrame(step);
  }

  var observer = new IntersectionObserver(function (entries) {
    entries.forEach(function (entry) {
      if (entry.isIntersecting) {
        run();
        observer.disconnect();
      }
    });
  }, { threshold: 0.4 });

  observer.observe(card);
})();
CSS
.nuda-pr2-count-up {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px;
  width: 150px;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .5s ease,transform .5s ease;
}

.nuda-pr2-count-up.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.nuda-pr2-count-up__label {
  font: 600 9px ui-sans-serif,system-ui;
  color: #a1a1aa;
  text-transform: uppercase;
  letter-spacing: .08em;
}

.nuda-pr2-count-up__price {
  display: flex;
  align-items: baseline;
  gap: 2px;
}

.nuda-pr2-count-up__price i {
  font: 500 11px ui-sans-serif,system-ui;
  font-style: normal;
  color: #63636e;
}

.nuda-pr2-count-up__num {
  font: 800 28px ui-sans-serif,system-ui;
  color: #fafafa;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.nuda-pr2-count-up__num::before {
  content: "$";
}

.nuda-pr2-count-up__sub {
  font: 500 10px ui-sans-serif,system-ui;
  color: #63636e;
}

.nuda-pr2-count-up.is-done .nuda-pr2-count-up__num {
  animation: nuda-pr2-count-up-flash .5s ease;
}

@keyframes nuda-pr2-count-up-flash {
  0% {
    filter: drop-shadow(0 0 0 rgba(228,255,84,0));
  }
  40% {
    filter: drop-shadow(0 0 10px rgba(228,255,84,.8));
  }
  100% {
    filter: drop-shadow(0 0 0 rgba(228,255,84,0));
  }
}

@media (prefers-reduced-motion:reduce) {
  .nuda-pr2-count-up {
    transition: none;
    opacity: 1;
    transform: none;
  }
  .nuda-pr2-count-up.is-done .nuda-pr2-count-up__num {
    animation: none !important;
  }
}

How to use Count-Up Price Reveal

Paste the HTML where you need it and the CSS into a global stylesheet (or a <style> tag). Every class is prefixed nuda- so it never collides with Tailwind or your own styles. Tweak the CSS custom properties to match your design system.

Works in React, Vue, Svelte, Astro, Next.js, Nuxt, Laravel Blade, Django, Rails — or a single .html file. No npm install, no build step.

More pricing tables components

← Browse all NudaUI components