Skip to content
animorio
IT
All components

The odometer pricing

Pricing, Transitions, 3DGSAP, CanvasAdvancedCompressed weight: Demo 13 KBGSAP + CustomEase 31 KBFreeDownload the zip

The plan picker of a backup service, with every price on the wheels of an odometer: switching from monthly to yearly, or choosing a plan, the wheels really count to the new price, carrying from one wheel to the next, blurred while they spin, and land on it with a little spring; the total due today runs like a fuel pump.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The odometer pricing</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/sofia-sans@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/sofia-sans-condensed@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the plans of a backup service. Every price stands on the wheels of a mechanical counter, drawn by the script:
         changing the billing or the plan, the wheels run to the new price like an odometer, carrying from one wheel
         to the next, blurred while they spin, and land on it with a little spring -->
    <section class="picker" aria-labelledby="picker-title" data-picker data-autoclic>
      <header class="picker__head">
        <h2 class="picker__title" id="picker-title">Choose a plan</h2>
        <p class="picker__sub">Backed up every night, kept far from home.</p>
      </header>

      <div class="billing" role="radiogroup" aria-label="Billing" data-billing>
        <span class="billing__thumb" aria-hidden="true" data-thumb></span>
        <span class="billing__row">
          <button class="billing__opt" type="button" role="radio" aria-checked="true" data-period="month">Monthly</button>
          <button class="billing__opt" type="button" role="radio" aria-checked="false" tabindex="-1" data-period="year">Yearly <span class="billing__save" data-save>Save 20%</span></button>
        </span>
        <!-- the same words in ink, cut to the size of the thumb: they change colour exactly at its edge -->
        <span class="billing__row billing__row--ink" aria-hidden="true" data-ink>
          <span class="billing__opt">Monthly</span>
          <span class="billing__opt">Yearly <span class="billing__save" data-save>Save 20%</span></span>
        </span>
      </div>

      <div class="plans" role="radiogroup" aria-labelledby="picker-title" data-plans></div>

      <div class="due">
        <div class="due__words">
          <span class="due__title" id="due-title">Due today</span>
          <span class="due__then" data-then><span class="due__then-in" data-then-in></span></span>
        </div>
        <div class="due__amount">
          <span class="due__cur" aria-hidden="true">€</span>
          <span class="wheels wheels--big" aria-hidden="true" data-meter></span>
          <span class="sr" data-due-sr></span>
        </div>
      </div>

      <button class="go" type="button" data-go>
        <span class="go__face" data-go-face>
          <svg class="go__spin" viewBox="0 0 20 20" aria-hidden="true" data-go-spin><path d="M10 2.5a7.5 7.5 0 1 1-7.5 7.5"/></svg>
          <span class="go__label" data-go-label>Continue with Household</span>
        </span>
      </button>

      <p class="sr" aria-live="polite" data-live></p>
    </section>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/CustomEase.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

53 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The odometer pricing” from Animorio and fit it into my site

What it does

On a white background a light card: Choose a plan, a line about the backups, a Monthly / Yearly control with the green chip Save 20%, three plans one under the other (Solo, Household with the chip Most chosen, Studio) and at the bottom Due today with the total and the button Continue with Household. Every price sits in the little window of a mechanical counter: dark wheels for the euros, light ones for the cents, with the point printed between them and the nearby digits peeking out above and below. When the page opens every counter starts at zero and climbs to its price. Switching to Yearly the white thumb slides under the word and the writing turns dark exactly at its edge; the plans’ prices run down one after the other (from €11.99 to €9.59) and the total runs up to the whole year (€115.08): the cents wheel turns all the time, the one on its left only moves while that one goes from 9 to 0, the fastest wheels become blurred streaks, then everything slows down, goes a hair past the price and comes back. Under Due today what you save arrives in green. Choosing a plan its box fills and the tick draws itself, its wheels give a little shake, the plan opens on what it includes while the one left closes, the total runs to the new price and the button changes its name. Pressed, the button shows Opening checkout with a turning arc and then goes back as it was.

How it works

A real odometer

The value is a number of cents that moves smoothly, and every wheel is worked out from it: the cents wheel is the value itself, every other wheel is the whole part of the value divided by its power of ten, plus how far the rest has gone past the last unit before the turn (from 0.99 to 1.00 the units turn together with the last cent). So the carries ripple up the row as in a machine, and a price going from €11.99 to €115.08 really counts every cent in between.

Digits on a cylinder

Each wheel is a cylinder with ten digits a tenth of a turn apart, drawn on a canvas: a digit turned by an angle φ sits R·sin φ from the middle and is squashed by cos φ in height, and a gradient darkens the wheel towards the top and the bottom, where it turns away. The cents have light wheels, like the tenths wheel of an odometer; a leading zero is a blank face, which fills in together with the carry of the wheel on its left.

A real blur

While the wheels move, each frame is drawn several times along the time the shutter stays open (BLUR frames), one copy for every pixel and a half of travel, and the copies are added up on a clear sheet, which is then dyed the colour of the digits. A wheel covering more than a few digits a frame becomes the streaks its ten digits would leave: for each column of pixels the share of a turn that is ink, worked out once from the real digits.

Run and landing

The run follows the RUN curve in SPIN seconds to LAND[0] cents past the price, then a damped spring brings it back, starting from rest. The value can be worked out for any moment, not step by step: that is what lets the blur look a little into the past. Choosing a plan its wheels give a shake that goes backwards first, so a price ending in 9 does not set off the carry at once.

The plan chosen

The plans are a radiogroup with the arrows, Home and End; the plan chosen opens on what it includes while the one before closes with the same curve and the same duration, and they all open to the height of the tallest list, so the card never changes size. The Monthly / Yearly control has on top a copy of its words in black, clipped with clip-path to the size of the thumb: the colour changes exactly at its edge.

When nothing moves

A single loop on GSAP’s ticker redraws only the counters that are moving, once more when they stop, and then goes to sleep. A click from the library’s preview (detail 0) on the card switches between monthly and yearly and to the next plan in turn; an aria-live region reads out the new prices and the total.

Values to change

NameFileDefaultWhat it changes
PLANSscript.js[['Solo', '1 computer, 1 TB', 4.99, […]], …]The plans: name, what it covers, price a month in euros, what comes with it.
PICKscript.js1The plan recommended, chosen when the page opens (0 is the first).
SAVEscript.js20How much less a month costs when paying yearly, in per cent.
SPINscript.js1.1The seconds the wheels take to run to the new price.
RUNscript.js0.66, 0, 0.2, 1How the wheels speed up and slow down.
LANDscript.js[0.22, 15]The landing: how many cents the wheels go past the price and how stiff the spring bringing them back is.
BLURscript.js1The blur of the spinning wheels: for how many frames the shutter stays open (0 = none).
STAGGERscript.js0.07The seconds between one price setting off and the next.
OPENscript.js0.5The seconds a plan takes to open on what it includes.
SIZEscript.js[20, 32]The height of the digits on the wheels, in the plans and in the total, in pixels.

Try them in the Playground

When to use it

For choosing the plan of a subscription: the pricing page of an app or a service, the window for upgrading to a higher plan, the summary before paying. It suits places where the price should feel like a real reckoning, and where the yearly saving has to show.

On phones

At 390 pixels the card fits in 358, with the plans one under the other and the counters the same size; Most chosen wraps under the name. A tap chooses a plan like a click, the plans sink a touch under the finger and two quick taps do not zoom the page.

With reduced motion

The wheels do not spin: the new price is in place at once and the counter fades in; the thumb goes to its place without sliding, the plans open and close at once and what they include fades in, and the line under Due today changes with a fade.

Pitfalls

  • Drawing the blur copies one over the other at an opacity of 1/n, at the end of a run the digits would stay at two thirds and then snap to full: opacities do not add up. The copies have to be added (lighter) on a clear sheet, then dyed with source-in.
  • A wheel turning hundreds of times a second, sampled a few times a frame, would show random flickering digits instead of a blur: past a few digits a frame it switches to the streaks worked out from the ink of the ten digits.
  • A landing past the price made with back.out, or with a spring on the whole run, overshoots in proportion to the distance: from €11.99 to €115.08 the cents wheel would make whole extra turns. The extra bit has to be given in cents, the same for every run.
  • With prices ending in 9, going almost half a cent too far sets off the carry and for a moment the price seems to change: the landing is 0.22 cents, and the shake of the plan chosen goes backwards first.
  • A canvas does not make a font download by itself, and Fontsource files are split by alphabet: document.fonts.load is asked for the digits before measuring and drawing them, and the measures are redone when a new font arrives.
  • The lists of what is included, of different lengths, would make the card change height while one plan opened and the other closed: they all open to the height of the tallest, on the same curve.

Libraries

GSAP 3.15.0, Sofia Sans (font variabile) 5.3.0, Sofia Sans Condensed (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP + CustomEase 31 KB.

Testing

Tested on 6 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components