webdevpuneet.com

Number to Word Morph Counter — Animated Count-Up JS

Number to Word Morph Counter · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="scene">
  <p class="sub">Trusted by</p>
  <div class="morph-stage">
    <span id="display" class="morph-value">0</span>
    <span class="morph-suffix">teams</span>
  </div>
  <button class="replay" onclick="playCounter()">Replay</button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Counts up numerically first, then cross-fades into the spelled-out word form
numberToWords() is a self-contained recursive number-to-English converter
Handles ones, teens, tens, hundreds, and thousands compositionally
Cross-fade swap replaces textContent only while fully transparent — no flicker
Whole sequence written as one readable async/await function, no callback nesting
Replay button re-runs the full count-up-then-morph sequence from zero
Zero dependencies — no counting or animation library required
Fully customizable target number, suffix label, and timing constants
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

Number to Word Morph Counter — Count-Up Then Number-to-Words Cross-Fade

Screenshot of the Number to Word Morph Counter snippet rendered live

A number-to-word morph counter first counts up like a standard number ticker or odometer stat counter, then — once it reaches its final value — cross-fades into the same number spelled out as a word ("248" becomes "Two hundred forty-eight"). The two-stage reveal gives a statistic both the punch of a fast numeric count-up and the warmth of plain language, which plain digits alone do not communicate as well in a marketing context.

The count-up phase

playCounter() runs a simple stepped loop: steps = 14, and on each iteration it computes value = Math.round((TARGET / steps) * i) and writes it directly into display.textContent, pausing 45ms between steps with await new Promise((r) => setTimeout(r, 45)). This is a linear count rather than an eased one — every step advances the same amount — which keeps the code simple and reads clearly as "counting" rather than a decelerating stat-ticker animation.

Converting a number into English words

numberToWords(n) is a small recursive number-to-English converter built from three lookup arrays: ones (zero through nine), teens (ten through nineteen), and tens (the tens-place words: twenty, thirty, etc). For numbers under 10 it indexes ones directly; for 10-19 it indexes teens; for 20-99 it combines a tens word with a hyphenated ones remainder ("forty-eight"); for 100-999 it recurses on the remainder after subtracting the hundreds ("two hundred forty-eight"); and for 1000+ it recurses again for the thousands group. This mirrors how the number is actually said aloud in English, built up compositionally rather than from one giant lookup table.

The cross-fade swap

swapText(newText) adds a .swap class that fades the number out and drops it slightly (opacity: 0; transform: translateY(10px) scale(0.9)) over 180ms, waits for that transition to finish via a matching setTimeout, replaces the textContent while the element is invisible, then removes .swap to fade the new text back in. Because the DOM swap happens exactly at the moment of zero opacity, there is no visible flash or wrong-content flicker — the morph looks like one continuous, smooth transformation from digits to words.

Sequencing with async/await

The whole sequence — count up, hold, then morph to words — reads top to bottom as a single async function playCounter() using await on each timed step. This avoids the nested-callback "pyramid" that chaining several setTimeout calls would otherwise produce, and makes the "Replay" button trivial: it just calls playCounter() again, which resets display.textContent to '0' before re-running the whole sequence.

Step by step

How to Use

  1. 1
    Watch it load, then replayThe counter counts from 0 to its target value, then morphs into the spelled-out word. Click "Replay" to run the whole sequence again.
  2. 2
    Change the target numberIn the JS panel, update const TARGET = 248 to any positive integer — numberToWords() handles values into the thousands.
  3. 3
    Adjust the count-up speedChange steps (currently 14) or the 45ms delay inside the for loop in playCounter() — more steps and a shorter delay produce a smoother, faster count.
  4. 4
    Change the pause before the morphUpdate the 500ms setTimeout between the count-up finishing and swapText() being called.
  5. 5
    Change the suffix labelEdit the "teams" text inside .morph-suffix in the HTML panel to match your own stat (users, downloads, stars, and so on).
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Landing page trust and social-proof stats
Show a user, download, or customer count that counts up and then resolves into words for a more human, less spreadsheet-like feel than raw digits alone.
Annual report and impact number reveals
Nonprofit and impact-focused sites benefit from spelling out a milestone number ("One million meals served") for warmth and emphasis after the count-up grabs attention.
Learn recursive number-to-word conversion
Edit numberToWords() in the JS panel and test different TARGET values to see how the recursive breakdown into hundreds, tens, and ones composes the final phrase.
Onboarding milestone celebrations
Celebrate a user reaching a milestone (100th task completed, 50th day streak) by counting up to the number and then morphing it into words for extra warmth.
Pair with a scroll-triggered counter
Combine with an IntersectionObserver (see the pattern in 3D character flip reveal) so playCounter() only fires once the stat scrolls into view.
Pricing and plan comparison stat call-outs
Highlight a key number (seats included, storage limit) with the count-up-then-word treatment to make a plan feature more memorable than a plain number.

Got questions?

Frequently Asked Questions

It sees 248 is under 1000 and over 99, so it takes the hundreds digit (2, giving "two hundred") and recurses on the remainder (48). The recursive call on 48 sees it is 20-99, so it combines the tens word ("forty") with a hyphenated ones word ("eight"), producing "forty-eight". The two pieces join as "two hundred forty-eight".

swapText() adds the .swap class, which fades opacity to 0 over 180ms, and only replaces textContent after awaiting that same 180ms via setTimeout. Swapping the text while the element is fully transparent means the viewer never sees the old and new text overlap or flash.

The count-up here is intentionally simple and linear (equal increments per step) to keep the code easy to read and to keep the count-up phase visually distinct from the smoother cross-fade morph that follows it. Swap in an eased formula, such as easeOutQuad on the step progress, for a decelerating count instead.

Yes, up into the low thousands — it detects values of 1000 or more, recursively converts the thousands group, appends "thousand", and recurses again on the remainder below 1000. It does not currently handle millions; extend the pattern with another tier if you need larger numbers.

In playCounter(), remove the for loop entirely and set display.textContent = String(TARGET) directly before the pause and swapText() call.

Yes. Click "JSX" for a React component. Move numberToWords() to a plain utility function, drive the count-up and swap phases from useState plus useEffect with async helper functions, and guard against state updates after unmount with a cleanup flag.