GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Number Rolling Animation JS: 9 Odometer Scenes

Number rolling animation js: Why does a number feel more trustworthy rolling than jumping?

A Number Is a Rolling Strip, Not a Changing Value: Number Rolling Animation JS Starts by Pushing a 0 to 9 Strip Up by Its Value in Each Place

Auto-plays · click a tile to jump to its section · all nine in one zip

Why a twelve-cell strip stays unbroken from 9 to 0

In 01 to 05, 08 and 09 one place is one window, and inside it sits a strip of twelve cells, 0 to 9 followed by 0 and 1 again. The strip moves by translateY(-(pos mod 10)/12·100%), so a 0 is already waiting after the 9 and the strip keeps going up without rewinding. All nine scenes set tabular-nums, so digits of different widths cannot make a place jitter.

Count the cells to roll first, then fit only the end to the target

The scenes count how many cells to roll instead of treating a digit as a value. 01 rolls 10 + mod(b−a, 10) cells, one lap extra, 03 starts at 30 + 6i cells and adds 0 to 9 to land on the target, and the hour wheel of 07 turns 12 to 23 cells. A long distance is what builds speed, and speed is what gives blur and a spring something to do.

Where this differs from the neighboring posts

A number counter animation codepen is a fine place to try a quick idea, and these files go a step further by keeping speed, blur and carry in a script timetable. The CSS number counter post covers counting up with CSS alone, while this one computes per-place speed, smear and carry in JS. Hand-operated inputs are in the number stepper post, time running down is in the countdown timer post, and a spinning wheel that stops by friction like 08 is in the roulette post.

Number Rolling Animation JS: 9 Scenes — Digit Strips Rolling to a Stop per Place, a Number That Overshoots and Springs Back, a Reel That Blurs When Fast, a Counter Whose Carry Cascades, a Price That Flashes by Direction, a Split-Flap Board, Digits on a Round Drum, an Inertial Friction Dial and a Hero Stat Line

01 to 05 put a different rule on the same vertical strip: place delay, spring, speed blur, carry and direction. 06 drops the strip and folds two leaves, and 07 squashes digits by the angle of a cylinder. 08 lands on a tick by friction, and 09 ties the place delay of the first scenes into one hero line. Every scene is a number counter animation free of libraries, WebGL and CDN calls, driven by a clock counted from the requestAnimationFrame timestamp.

What a 2-second capture of the stage measured

Each scene was rendered and 24 frames were captured, which leaves 23 neighbor differences to count.

Scene Area covered Frames that moved Mean intensity
01 Digit strips per place 13.2% 13/23 164.6
02 Spring settle 15.7% 14/23 148.9
03 Speed-blur reel 12.3% 21/23 84.7
04 Carry cascade 11.1% 18/23 132.3
05 Direction price 22.9% 15/23 120.9
06 Leaf flip 10.6% 15/23 117.7
07 Drum perspective 13.9% 16/23 88.5
08 Friction dial 6.2% 10/23 105.2
09 Stat line 21.6% 16/23 143.3

The widest change is 05 at 22.9%, where the whole price panel flashes, and the narrowest is 08 at 6.2%, where the dial snaps to a tick and soon stops. 03 moved in the most frames, 21 of 23, because five reels keep spinning, and 08 moved in the fewest, 10 of 23. Mean intensity peaks at 164.6 in 01 and bottoms out at 84.7 in 03.

01Digit Strips That Roll and Stop Per Place

Put it on a cumulative visitors card and five places start at 0.25 seconds from the ones place, so the right-most digit stops first and the left ones follow until 1.67 seconds. placeU(k) delays the progress of place k (0 is the ones place) by PLACE_DELAY_MS 80 ms each, and the cells to roll are 10 + mod(b−a, 10). The small row beneath is posBad, which uses the digit value itself as the strip position and sweeps backward when it goes to a smaller digit.

Digit Strips That Roll and Stop Per Place
Open in a new tab

01 · Digit Strips That Roll and Stop Per Place — Measured area 13.194% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLACE_DELAY_MSROLL_SplaceU
    function placeU(k) {
        return (sceneT - START_S - k * PLACE_DELAY_MS / 1000) / ROLL_S;
    }
    function draw() {
        const a = digitsOf(fromV, PLACES);
        const b = digitsOf(toV, PLACES);
        for (let i = 0; i < PLACES; i += 1) {
            const k = PLACES - 1 - i;
            const u = clamp(placeU(k), 0, 1);
            // run one extra lap (10 cells) and stop on the target digit
            const adv = 10 + mod(b[i] - a[i], 10);
            const pos = a[i] + adv * easeOut3(u);
            const speed = u > 0 && u < 1 ? adv * 3 * (1 - u) * (1 - u) / ROLL_S : 0;
            moveStrip(strips[i], pos, speed);
            glowStrip(strips[i], speed / BLUR_REF * 0.9);
            // the rewinding one: the digit value is used as the strip position, so going to a smaller digit sweeps backward
            const posBad = lerp(a[i], b[i], easeOut3(u));
            moveStrip(naive[i], posBad, 0);
            glowStrip(naive[i], u > 0 && u < 1 && b[i] < a[i] ? 0.8 : 0);
        }
    }

02A Number That Overshoots and Springs Back to Settle

Use it for the total on a payment complete screen, where the number runs a little past the target and springs back to settle. update cuts each frame into ceil(dt·240) slices and accumulates acc = K(goal−x) − C·v, so a long frame cannot blow the spring up. At SPRING_K 120 and SPRING_C 12 the damping ratio is 0.548, the overshoot is 12.8% of the distance, and the first arrival comes 0.23 seconds after the start.

A Number That Overshoots and Springs Back to Settle
Open in a new tab

02 · A Number That Overshoots and Springs Back to Settle — Measured area 15.678% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPRING_KSPRING_CPLACE_DELAY_S
    function update(dt) {
        // accumulate in 240Hz slices so a long frame cannot blow the spring up
        const n = Math.max(1, Math.ceil(dt * 240));
        const h = dt / n;
        for (let i = 0; i < springs.length; i += 1) {
            const sp = springs[i];
            if (sceneT < sp.start) {
                continue;
            }
            for (let j = 0; j < n; j += 1) {
                const acc = SPRING_K * (sp.goal - sp.x) - SPRING_C * sp.v;
                sp.v += acc * h;
                sp.x += sp.v * h;
            }
        }
        const one = springs[PLACES - 1];
        if (sceneT >= one.start) {
            peakOver = Math.max(peakOver, one.x - one.goal);
        }
    }

03A Reel That Blurs When Fast and Sharpens as It Slows

On an event draw reveal screen, digits are unreadable while the reels spin and sharp once they stop. moveStrip divides speed by BLUR_REF 60 to get sp, lowers the main strip's opacity to 1 − 0.5·sp and lays two tail strips sp·BLUR_LAG·g cells behind to build the smear. Five reels stop from 1.2 seconds, STOP_STEP 0.24 seconds apart from the left, and reelDist sets the cells to roll as 30 + 6i plus 0 to 9.

A Reel That Blurs When Fast and Sharpens as It Slows
Open in a new tab

03 · A Reel That Blurs When Fast and Sharpens as It Slows — Measured area 12.345% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

STOP_STEPreelDistBLUR_REF
    // pos = how many cells the strip is pushed up now (fractional), speed = cells per second (signed). With tail strips the smear is proportional to speed
    function moveStrip(s, pos, speed) {
        putCol(s.cols[0], pos);
        if (s.cols.length < 2) {
            return;
        }
        const sp = clamp(Math.abs(speed) / BLUR_REF, 0, 1);
        const dir = speed < 0 ? -1 : 1;
        s.cols[0].style.opacity = (1 - 0.5 * sp).toFixed(3);
        for (let g = 1; g < s.cols.length; g += 1) {
            putCol(s.cols[g], pos - dir * sp * BLUR_LAG * g);
            s.cols[g].style.opacity = (0.5 * sp / g).toFixed(3);
        }
    }

04A Counter Whose Carry Cascades to the Next Place

On a live concurrent-users counter, going from 999 to 1000 lifts four places one after another. placePos(k, lag) starts from the place's share of the start value, adds one cell for every tick that changed that place and delays place k by k·lag. The upper row uses CARRY_LAG_S 0.07 seconds, so the ones, tens, hundreds and thousands places start at 1.08, 1.15, 1.22 and 1.29 seconds.

A Counter Whose Carry Cascades to the Next Place
Open in a new tab

04 · A Counter Whose Carry Cascades to the Next Place — Measured area 11.077% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

CARRY_LAG_SplacePoschainOf
    function tenPow(k) { return Math.pow(10, k); }
    // cells rolled in place k: the place's share of the start value plus one cell for every tick that changed it (delayed in carry order)
    function placePos(k, lag) {
        let pos = Math.floor(v0 / tenPow(k));
        for (let j = 1; j <= TICKS; j += 1) {
            if (Math.floor((v0 + j) / tenPow(k)) !== Math.floor((v0 + j - 1) / tenPow(k))) {
                const s = FIRST_S + (j - 1) * TICK_S + k * lag;
                pos += easeOut3((sceneT - s) / ROLL_S);
            }
        }
        return pos;
    }

05A Price That Rolls Up or Down and Flashes by Direction

On an exchange-rate or stock-quantity board the strip rolls up when the value rises and down when it falls, so direction reaches the eye first. When goesUp() is true it rolls mod(b−a, 10) cells forward, otherwise −mod(a−b, 10) backward, and the panel switches between is-up (mint) and is-down (pink). The flash opacity decays as 0.9·exp(−(t−0.2)/0.35).

A Price That Rolls Up or Down and Flashes by Direction
Open in a new tab

05 · A Price That Rolls Up or Down and Flashes by Direction — Measured area 22.903% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLASH_TAUgoesUpSTAGGER_S
    function goesUp() { return toV > fromV; }
    function fixed2(v) { return (v / 100).toFixed(2); }
    function placeU(k) { return (sceneT - START_S - k * STAGGER_S) / ROLL_S; }
    function draw() {
        const a = digitsOf(fromV, PLACES);
        const b = digitsOf(toV, PLACES);
        const up = goesUp();
        panel.classList.toggle(cls('is-up'), up);
        panel.classList.toggle(cls('is-down'), !up);
        for (let i = 0; i < PLACES; i += 1) {
            const k = PLACES - 1 - i;
            const u = clamp(placeU(k), 0, 1);
            // up rolls forward (upward), down rolls backward (downward): follow the direction of the value, not the shortest way
            const adv = up ? mod(b[i] - a[i], 10) : -mod(a[i] - b[i], 10);
            const pos = a[i] + adv * easeOut3(u);
            const speed = u > 0 && u < 1 ? adv * 3 * (1 - u) * (1 - u) / ROLL_S : 0;
            moveStrip(strips[i], pos, speed);
            glowStrip(strips[i], 0);
        }
        const since = sceneT - START_S;
        flash.style.opacity = (since >= 0 ? 0.9 * Math.exp(-since / FLASH_TAU) : 0).toFixed(3);
    }

06A Split-Flap Board Whose Leaves Fold Over

Like an arrival time board, each cell flips through several digits before it lands on the target. flipsOf(a, b) returns 5 + mod(b−a−5, 10) leaves, and in the first half of a leaf the top one folds with rotateX(−180u°) while in the second half the bottom one unfolds from 90 to 0 degrees. Going from 0618 to 0847 flips 10, 12, 13 and 9 leaves per cell, 44 in all, and the third cell stops last at 1.75 seconds.

A Split-Flap Board Whose Leaves Fold Over
Open in a new tab

06 · A Split-Flap Board Whose Leaves Fold Over — Measured area 10.572% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLIP_SflipsOfCELL_DELAY_S
    function draw() {
        const a = digitsOf(fromV, 4);
        const b = digitsOf(toV, 4);
        for (let i = 0; i < 4; i += 1) {
            const total = flipsOf(a[i], b[i]);
            const e = clamp(elapsed(i), 0, total - 0.0001);
            const s = Math.floor(e);
            const u = e - s;
            const cur = mod(a[i] + s, 10);
            const nxt = mod(a[i] + s + 1, 10);
            const c = cards[i];
            setNum(c.top, nxt);
            setNum(c.bot, cur);
            setNum(c.ft, cur);
            setNum(c.fb, nxt);
            // in the first half the top leaf folds forward down to -90 degrees, in the second half the bottom leaf unfolds from 90 to 0 degrees
            const front = u < 0.5;
            c.ft.style.opacity = front ? (1 - u * 0.5).toFixed(3) : '0';
            c.ft.style.transform = 'rotateX(' + (front ? (-180 * u).toFixed(2) : '-90') + 'deg)';
            c.fb.style.opacity = front ? '0' : '1';
            c.fb.style.transform = 'rotateX(' + (front ? '90' : (90 - 180 * (u - 0.5)).toFixed(2)) + 'deg)';
            c.shade.style.opacity = (0.45 * Math.sin(Math.PI * u)).toFixed(3);
        }
    }

07Digits Squashed by the Perspective of a Round Drum

In an hour and minute selection wheel, digits sit around a cylinder and the angle squashes them, so only the middle one is big and bright. For each cell θ = (idx−pos)·STEP_DEG 30 degrees gives translateY R·sinθ, scale(0.7+0.3cosθ, cosθ) and opacity cos²θ, and back-side cells with cosθ at or under 0.05 are not drawn. From 5:52 to 7:25 the hour wheel turns 14 cells and stops at 1.55 seconds, and the minute wheel turns 33 cells and stops at 1.95 seconds.

Digits Squashed by the Perspective of a Round Drum
Open in a new tab

07 · Digits Squashed by the Perspective of a Round Drum — Measured area 13.901% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

STEP_DEGRADIUSdistOf
            const e = w.items[j + half];
            const c = Math.cos(th);
            if (c <= 0.05) {
                e.style.opacity = '0';
                continue;
            }
            const text = String(mod(idx, w.n) + w.offset).padStart(2, '0');
            if (e.textContent !== text) {
                e.textContent = text;
            }
            // height is squashed by cos θ and width shrinks a little; the fade is cos θ squared times the speed blur
            // convert the distance from the center (share of wheel height) to a share of the item's own height (%) for translateY
            const y = RADIUS * Math.sin(th) * 100 / (ITEM_H / 100);
            e.style.transform = 'translateY(' + y.toFixed(2) + '%) scale(' + (0.7 + 0.3 * c).toFixed(3) + ',' + c.toFixed(3) + ')';
            e.style.opacity = (c * c * fade).toFixed(3);

08An Inertial Dial That Slows by Friction and Snaps to a Tick

As a dial for entering a quantity or age, it takes a starting speed, glides and lands on the nearest tick. step multiplies v *= FRICTION^(dt·60) every frame and, once speed falls under SNAP_V 3, pulls toward the Math.round(x) tick by 1 − e^(−14dt) per step. Started at 90 cells per second, the pull begins at 0.68 seconds and the dial goes 18 cells from 2 to 20, which reads 0, stopping at about 1.07 seconds.

An Inertial Dial That Slows by Friction and Snaps to a Tick
Open in a new tab

08 · An Inertial Dial That Slows by Friction and Snaps to a Tick — Measured area 6.247% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

FRICTIONSNAP_VSPIKE_EXTRA
    function step(d, dt) {
        if (!d.flicked && sceneT >= START_S) {
            d.flicked = true;
            d.v = FLICKS[cycle % FLICKS.length];
        }
        if (!d.flicked) {
            return;
        }
        if (!d.snapping) {
            d.x += d.v * dt;
            d.v *= Math.pow(FRICTION, dt * 60);
            if (Math.abs(d.v) < SNAP_V) {
                d.snapping = true;
                d.snapTo = Math.round(d.x);
                d.v = 0;
            }
        }
        else {
            d.x += (d.snapTo - d.x) * (1 - Math.exp(-SNAP_RATE * dt));
            if (Math.abs(d.snapTo - d.x) < 0.002) {
                d.x = d.snapTo;
            }
        }
    }

09A Hero Stat Line That Rolls Up One After Another

On the first screen of a service landing page, visitors, reviews and rating enter in turn and roll up place by place. startOf(i) brings the cards in at 0.2, 0.5 and 0.8 seconds, statU(i, k) delays each place by PLACE_DELAY_S 0.05 seconds from the lowest, and each place rolls from 0 by 10 + its target digit cells. The rating shows the integer 48 divided by 10 as 4.8, and everything finishes at 2.03 seconds.

A Hero Stat Line That Rolls Up One After Another
Open in a new tab

09 · A Hero Stat Line That Rolls Up One After Another — Measured area 21.611% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

ENTER_GAP_SPLACE_DELAY_SstatU
    function startOf(i) { return START_S + i * ENTER_GAP_S; }
    function statU(i, k) { return (sceneT - startOf(i) - FADE_S * 0.5 - k * PLACE_DELAY_S) / ROLL_S; }
    function draw() {
        for (let i = 0; i < 3; i += 1) {
            const st = stats[i];
            // rises from low on entry, with a floor brightness so it is faintly visible from the start
            const e = easeOut3((sceneT - startOf(i)) / FADE_S);
            st.card.style.opacity = (0.35 + 0.65 * e).toFixed(3);
            st.card.style.transform = 'translateY(' + ((1 - e) * 14).toFixed(2) + '%)';
            const d = digitsOf(target[i], st.digits);
            for (let j = 0; j < st.digits; j += 1) {
                const k = st.digits - 1 - j;
                const u = clamp(statU(i, k), 0, 1);
                const adv = 10 + d[j];
                const speed = u > 0 && u < 1 ? adv * 3 * (1 - u) * (1 - u) / ROLL_S : 0;
                moveStrip(st.strips[j], adv * easeOut3(u), speed);
                glowStrip(st.strips[j], speed / BLUR_REF * 0.8);
            }
        }
    }

How Many Places and How Slowly Should Number Rolling Animation JS Roll to Stay Readable?

Every place stops inside 2.16 seconds

With the place delay included, the time all places have stopped is 1.67 seconds in 01, 1.75 in 06, 1.95 in 07, 2.03 in 09 and 2.16 in 03. One place spends 1.0 to 1.1 seconds rolling in 01 and 09 and 0.7 seconds in 05, and the start gaps between places are 80 ms, 50 ms and 40 ms. Past five places, shrink the gap, or the left place stops late.

Screens where a value appears once and ends

A dashboard total or a payment complete sum shows one value and is done, which is what 01 and 02 fit. For a landing first screen, copy 09 and space the cards 0.3 seconds apart so one line becomes one scene.

Screens where the value keeps changing

The users counter (04), the price board (05) and the arrival board (06) keep changing the same places. The direction from the old value to the new one and the order of the carry must stay readable, so print the number of places that move together, like chainOf(n) in 04, on the status line.

Screens where a person picks the value

The hour and minute wheel (07) and the quantity and age dial (08) are controls, not displays. Both end at a tick or a groove, so read the value at the stopped position and put it in the form.

Push the Strip by the Value, Delay Each Place's Start, Stop with a Spring: The Order in Which One Number Rolls Into Place

The derivative of position is the blur

01, 03, 05 and 09 first compute the position as a + adv·easeOut3(u), then take its derivative adv·3(1−u)²/ROLL_S as the speed and pull blur and glow out of the same formula. The speed is highest at the start and 0 at the stop, so no separate slowing curve is needed. translateY alone pushes the strip, and the clock is counted from the time of requestAnimationFrame.

Place delay is stacked from the lowest place

With the ones place as k = 0 and the start delayed by k·PLACE_DELAY, the right place stops first. 02 sets the delay to 0.06 seconds and 05 to 0.04 seconds, so the gap between places follows the character of each scene.

The spring is stacked in 240Hz slices, not per frame

A long frame can blow the spring calculation up, so 02 cuts the frame into 240Hz slices, and the clock itself clamps one frame to DT_MAX 0.05 seconds. The scenes that accumulate position and speed frame by frame are 02 (spring) and 08 (friction), and the rest are drawn from the clock alone.

Where Number Rolling Breaks: Digits That Change One Place Without a Carry So 999 Reads 000, Numbers That Jump at the Strip End and Rewind and Numbers That Rush Together After a Hidden Tab

A late carry makes the value look wrong

The lower row of 04 uses a LATE_S gap of 0.3 seconds, so going from 999 to 1000 starts the ones, tens, hundreds and thousands places at 1.08, 1.38, 1.68 and 1.98 seconds. While the low places are back at 0 the upper places still sit at 9, and for a while the value reads wrong. The upper row has a 0.07 second gap, so that gap closes at 1.29 seconds.

A number that rewinds at the strip end

The small row of 01 uses lerp(a, b, easeOut3(u)), which takes the digit value itself as the strip position. Going from 9 to 2 sweeps the strip backward to where the 2 is, whereas rolling one extra lap and fixing the remainder with mod, as the upper row does, keeps the same direction.

A number that rushes together after a hidden tab

The small dial of 08 takes its long frame at 0.5 seconds as dt + SPIKE_EXTRA 0.4 seconds, uncut. In that frame v is multiplied in one go and the position jumps, so it stops on a different number from the dial above it.

    function update(dt) {
        step(good, dt);
        // the broken side takes one big step with no time cap
        let dtBad = dt;
        if (!bad.spiked && sceneT >= SPIKE_S) {
            bad.spiked = true;
            dtBad = dt + SPIKE_EXTRA;
        }
        step(bad, dtBad);
    }

A real hidden tab halts the loop of every scene; on return lastTs goes back to -1, which keeps the hidden time off the first frame. DT_MAX clamps a long frame once more if one still arrives. Unzip the archive with the password qyyem5sk, then pause the index.html files of the 01, 04 and 08 folders to see with your own eyes where each small row goes wrong.

Strengthen the Spring and How Much Does the Number Wobble? Spring Strength, Damping and Place Delay Compared

Name Value changed Feel
Stronger spring 02 SPRING_K 120 to 260 The damping ratio falls from 0.55 to 0.37, so the overshoot grows from 12.8% to 28.4% of the distance (about 1 cell to 2.2 cells for an 8-cell move, stepped at 1/60 second), the wobble frequency rises from 1.46Hz to 2.38Hz and the first arrival moves from 0.23 to 0.13 seconds
Longer place delay 01 PLACE_DELAY_MS 80 to 240 The left-most place starts 960 ms after the right-most instead of 320 ms, and the time all places have stopped grows by 0.64 seconds, from 1.67 to 2.31 seconds
Lower friction 08 FRICTION 0.92 to 0.97 The speed left after one second grows from 0.7% to 16.1%, the pull starts at 1.86 seconds instead of 0.68, and the glide grows from 18 cells to 48 cells

What to check before changing a value

In 02 SPRING_C stays at 12, so raising only SPRING_K leaves the settling time at a similar 0.7 to 0.9 seconds and only enlarges the overshoot. 01 still stops at 2.31 seconds with a 240 ms delay, which fits inside LOOP_S 3.4 seconds, but adding places eats that room. In 08 a friction of 0.97 starts the pull at 1.86 seconds, so less time is left to land on a tick inside LOOP_S 3.0 seconds.

One Final Number Under Reduced Motion: Accessibility Through a Sentence That Reads the Value and a Pause Button

Reduced motion and the scene description

Under prefers-reduced-motion: reduce, reduceQuery reads the media query, stops the loop and showRest() draws only the scene after REST_T seconds, the final number. The stage of each scene tells what happens with role="img" and aria-label, and the status pill (.vj__status) is aria-hidden so the rolling middle digits are kept out of the reading area. Only the first time isSettled() turns true does a hidden live region get the one sentence from finalText(), with aria-live="off" during the auto demo and polite once a person first touches it.

Keyboard and pause

The stage button rolls to the next value with nextNow() on click, Enter, Space, and the right and up arrow keys, and under reduced motion the same input draws the next final number at once. The pause button reports its state with aria-pressed, and visibilitychange stops the loop while the tab is hidden.

Number Rolling Animation JS FAQ: Rolling with CSS Only, Thousands Separators and Load When Values Change Often

Can a number counter animation generator or CSS alone give the same scenes?

If the value is fixed and the strip is pushed once by translateY, a CSS transition is enough. The spring settle, speed blur and carry cascade here read position and speed every frame to decide the next value, so they are computed in JS. There is no scroll detection (IntersectionObserver) in the source, and every scene runs on a timetable alone.

How do I add thousands separators?

fmtNum inserts a comma every three digits from the string length, and 09 keeps a separate comma cell with patterns such as dd,ddd, d,ddd and d.d. If the notation must change by country, build the string with Intl.NumberFormat first and then split it across the strips by the number of places.

Will it slow down when the value changes often?

The inline properties changed are mostly transform and opacity, and only the groove line top of 07 and the tick left of 08 are coordinate placement. If values arrive more often than one roll lasts (0.7 seconds in 05), it is simpler to swap the target to the latest value than to chain rolls.

Enter the archive password

The password is inside this article. You will find it as you read.