CountUp.js Pricing Toggle — update() vs new CountUp() Explained

CountUp.js Pricing Toggle · Pricing · Plain HTML, CSS & JS · Live preview

What's included

Features

Single reused CountUp instance
Constructed once at page load, never recreated on toggle — the core fix this snippet demonstrates.
Bidirectional .update()
The same method smoothly animates both up and down depending on the target relative to the current value.
No zero-flash bug
Because the instance retains its current value, toggling never visibly resets to 0 mid-transition.
Accessible toggle switch
role="switch" and aria-checked track state for assistive technology.
Synced label emphasis
The active billing period label is visually bolded alongside the switch state.
Configurable easing
useEasing: true keeps both count directions feeling equally polished.
Minimal state
A single isYearly boolean drives the toggle, label, suffix text, and counter target.
Clean suffix swap
The "/mo" vs "/mo, billed yearly" text updates alongside the animated number.

About this UI Snippet

CountUp.js Pricing Toggle — Why .update() Is the Right Tool, Not new CountUp()

Screenshot of the CountUp.js Pricing Toggle snippet rendered live

The naive way to animate a pricing toggle with CountUp.js is to construct a brand-new CountUp instance every time the user clicks — pass the new target price, call .start(). It works exactly once and then breaks the second time: every count-up starts its animation from 0, because a freshly constructed instance has no memory of what was previously displayed. Toggling monthly → yearly would show 0 → 23, correct enough by luck; toggling yearly → monthly again would show 0 → 29 — a visible flash down to zero before counting back up, on every single click. This snippet exists to show the actual fix.

One instance, created once

js var counter = new countUp.CountUp('cptPrice', MONTHLY, { duration: 0.6, useEasing: true }); counter.start();

The CountUp instance is created exactly once, outside the click handler, at module scope. It plays its normal first animation (0 → 29) on page load. From that point on, the instance is never recreated — it's held onto and reused for the lifetime of the page.

.update() animates from wherever the number currently is

js counter.update(target);

CountUp's .update(newEndVal) method tells the existing instance to animate from its current displayed value to a new target, using the same duration and easing configuration it was constructed with. Because the instance already knows its current value internally (it's tracking its own animation frame state), calling .update(23) while it's showing 29 produces a smooth 29 → 23 count-down, and calling .update(29) afterward produces a smooth 23 → 29 count-up — never a detour through zero. This is the entire fix, and it's a one-line difference from the broken version: reuse the instance and call .update() instead of constructing a new one.

CountUp naturally handles counting down, not just up

Despite the library's name, .update() works symmetrically in both directions — it's really "animate to a new value," and whether that's numerically higher or lower than the current one is irrelevant to the API. The easing (useEasing: true, the default) applies the same deceleration curve regardless of direction, so counting down from a higher plan price to a lower one feels exactly as polished as counting up.

Reusing it

This exact instance-reuse pattern is the right approach anywhere a displayed number needs to change repeatedly in response to user interaction — a shopping cart total as items are added/removed, a quantity stepper, a currency converter. The rule is always the same: construct CountUp once per numeric element, call .update() on every subsequent change.

Build with AI

Build, Understand, Optimize, and Extend It With AI

This snippet's whole value is a subtle lifecycle bug and its fix, so it rewards being asked to reproduce the broken version before appreciating the correct one. Paste the code into an AI assistant like Claude and ask it to first rewrite the toggle handler the "naive" way — constructing a new CountUp instance inside the click handler each time — and explain exactly why that version visibly flashes to $0 on every click. Then have it explain why .update() avoids that entirely. For extension, ask it to add a discount percentage badge that itself counts up when yearly is selected, animate the strikethrough monthly price alongside the yearly one for comparison, or generalize the single-price toggle into a 3-tier pricing table where all three prices update() in sync on one shared toggle.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "monthly/yearly pricing toggle" using CountUp.js (v2, from a CDN, global countUp.CountUp) in plain HTML, CSS, and JavaScript.

Requirements:
- A pricing card with a large animated price display (currency symbol, number, and a "/mo" suffix that changes text depending on billing period), a short feature list, and a CTA button.
- An accessible switch-style toggle (role="switch", aria-checked reflecting state) between "Monthly" and "Yearly (save 20%)" labels, with a sliding knob.
- Create the CountUp instance exactly ONCE, at page load (outside any click handler), and call .start() to animate the initial monthly price in from 0.
- On every toggle click, do NOT construct a new CountUp instance. Instead call the existing instance's .update(newValue) method with the appropriate price (a lower "yearly, shown as monthly-equivalent" value or the original monthly value). Add a code comment explicitly explaining that constructing a fresh CountUp on every click would reset its internal tracked value, causing the displayed price to visibly animate from 0 on every single toggle instead of smoothly transitioning between the two real numbers.
- Keep a single isYearly boolean as the source of truth, updating the toggle's aria-checked, both labels' active styling, the suffix text, and the counter target from it.
- Style it as a dark theme with a purple accent color, a centered pricing card with a soft shadow, and a smooth sliding toggle switch.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

Requires
<div class="cpt-stage">
  <div class="cpt-head">
    <span class="cpt-tag">countup.js · update(), not new CountUp()</span>
    <h2>Simple, Transparent Pricing</h2>
  </div>
  <div class="cpt-switch">
    <span class="cpt-switch-lbl is-on" id="cptMonthlyLbl">Monthly</span>
    <button class="cpt-toggle" id="cptToggle" role="switch" aria-checked="false">
      <span class="cpt-knob"></span>
    </button>
    <span class="cpt-switch-lbl" id="cptYearlyLbl">Yearly <em>save 20%</em></span>
  </div>
  <div class="cpt-card">
    <div class="cpt-price"><span>$</span><span id="cptPrice">29</span><span class="cpt-per" id="cptPer">/mo</span></div>
    <ul class="cpt-list">
      <li>Unlimited projects</li>
      <li>Priority support</li>
      <li>Team roles &amp; permissions</li>
      <li>Usage analytics</li>
    </ul>
    <button class="cpt-cta">Start Free Trial</button>
  </div>
</div>

Step by step

How to Use

  1. 1
    Add the CountUp.js CDNInclude the countUp.umd.js build from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSA pricing card renders with the monthly price, counting up from 0 on load.
  3. 3
    Click the toggleThe price smoothly animates to the yearly-equivalent figure using .update(), not a restart.
  4. 4
    Toggle backThe price counts back up to the monthly figure — never dropping to 0 in between.
  5. 5
    Change the price pointsEdit the MONTHLY and YEARLY_MONTHLY_EQUIV constants at the top of the script.
  6. 6
    Reuse the pattern elsewhereApply the same single-instance + .update() approach to a cart total or quantity stepper.

Real-world uses

Common Use Cases

SaaS pricing pages
The canonical monthly/yearly toggle, made to feel alive instead of an instant text swap.
Shopping cart totals
The same update() pattern smoothly animates a total as line items change.
Currency/unit converters
Reuse one instance per output field and update() on every input change.
Quantity steppers
A stepper input whose displayed value animates on each increment/decrement.
Plan comparison sliders
A price that recalculates and animates as a usage slider moves.
Learning CountUp lifecycle
A focused example of instance reuse versus reconstruction, applicable to any stateful animation.

Got questions?

Frequently Asked Questions

A freshly constructed CountUp instance has no memory of what value is currently displayed, so it always animates FROM 0 by default. Every toggle click would show the price flash down to $0 and count back up, rather than transitioning smoothly between the two real price points — which is the exact bug this snippet's approach avoids.

The CountUp instance tracks its own current displayed value internally as part of its animation state. Calling .update(newValue) tells it to animate from that internally-tracked current value to the new target, using the same duration and easing it was originally configured with — no need to pass or track the starting value yourself.

Yes — despite the library's name, update() is direction-agnostic: it animates to whatever value you pass, whether that's higher or lower than the current one. The easing curve applies symmetrically, so a yearly-to-monthly toggle (price going up) looks as smooth as monthly-to-yearly (price going down).

It applies CountUp's default easing function (an ease-out curve) to the count animation instead of a linear frame-by-frame increment, so the number decelerates as it approaches its target rather than ticking up at a constant rate. It's on by default but set explicitly here for clarity.

A single source of truth in JS avoids having to parse the toggle's aria-checked attribute or CSS class back out every time you need to know the current state; it's simpler to drive the DOM attributes and labels FROM the JS variable each click than to derive the variable from the DOM.

Create the CountUp instance once in a useEffect/onMounted with an empty dependency array (so it truly runs only once), store it in a ref, and call counter.current.update(newValue) from your toggle handler instead of recreating the instance on every state change — the same core rule applies regardless of framework.