Faking an Infinite Display Loop

22 Jan 2026

Table of Contents


Introduction

What do I mean by “an infinite display loop”? Something like the display on one cog of an odometer:

“odometer”

There are plenty of examples of this around the web, but many of them are far more complicated than I wanted - and in some cases, difficult (for me) to understand.

In my case, there is no actual 3-dimensional wheel and nothing is actually rotating. It’s all done by using a simple list of digits, arranged vertically and some animations which give the illusion that the list is endless (which it most definitely isn’t).

Here is a bare-bones approach:

A Vertical Strip of Digits

Start with a <div> containing all the digits from 0 through 9 - and with an extra 0 again at the end (explained later, below):

HTML
1
2
3
<div class="digit">
  <div>0</div><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div><div>0</div>
</div>

Because a <div> is a block-level element in HTML, the related web page will default to displaying these digits in a single column down the page.

This gives us a vertical strip of digits.

We will be sliding this vertical strip up and down to simulate the rotation of a wheel, similar to one cog of an odometer.

Showing Only One Digit

Next, we place this into one additional containing div:

HTML
1
2
3
4
5
<div class="reel">
  <div class="digit">
    <div>0</div><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div><div>0</div>
  </div>
</div>

We use this outermost div as the target to apply the following style:

CSS
1
2
3
4
5
.reel {
  height: 1.1em;
  overflow: hidden;
  border: 1px solid grey
}

This creates a “window” onto our strip of digits. The window is slightly higher than the height of a character (1.1em). All content outside this window is hidden (overflow: hidden;). We therefore see one digit in the window: the 0 at the top of the strip.

If you change overflow: hidden; to overflow: visible;, you will see the rest of the strip of digits, outside of our viewing window:

“strip of digits”

Animating our Strip of Digits

The next step is to slide the strip of digits up and down through the viewing window.

I use the JavaScript Web Animations API for this (WAAPI for short).

To create a continuous smooth scroll, I use the following:

JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
digit.animate(
  [
    { offset: 1, transform: 'translateY(' + (height * -10) + 'px)' },
    { offset: 1, transform: 'translateY(0)' }
  ],
  {
    duration: 5000,
    iterations: 3
  }
);

The digit variable is the HTML node for <div class="digit"> - that is the HTML element we will be manipulating with our JS code.

The animate() method shown above contains two parts:

The configuration object tells us that one end-to-end animation will last for a duration of 5,000 milliseconds (5 seconds), and that this will be repeated 3 times.

Each keyframe in my code uses an offset value to place each instruction onto a timeline. Timelines start at 0 and end at 1 (think zero percent to 100 percent of the time assigned to the animation). So, in our case, that 1 means the transformation will begin at 0 milliseconds and last until 5000 milliseconds (it will last for 100% of the overall duration.) This gives us a fairly slow “turning of the wheel” for our digits display.

A translateY transformation moves the targeted div along its y-axis. In my case, the distance we shift is calculated elsewhere (that height variable - explained later).

This calculated height causes the div to move (slowly and smoothly) all the way to that extra <div>0</div> we have in our HTML - at the very bottom of the strip of digits.

The Never-Ending Loop Illusion

The first keyframe implicitly starts at 0 on the timeline. After the first keyframe has finished at offset: 1, the next keyframe in the array is instantaneously applied (because it is also at offset: 1). This second keyframe jumps us back to the top of the strip of HTML digits (displaying the very first <div>0</div> again). This jump is instantaneous because the first keyframe has already used up all of the timeline from 0 to 1 - so the second keyframe effectively starts and ends at the same point in time.

The iterations: 3 instruction means that the first keyframe starts again, as soon as final keyframe has ended.

This jump from the bottom 0 back to the top 0 is invisible to the naked eye - and this is what gives us the illusion of a never-ending reel of numbers, rotating through the visible window of our display.

To round off the above Javascript, here is how the required height of one digit is calculated:

JavaScript
1
2
3
digits = Array.from(document.querySelectorAll('.digit'));
divCount = digits[0].querySelectorAll('.digit > div').length;
height = digits[0].offsetHeight / divCount;

The main point here is the use of offsetHeight to get the actual rendered height of one div containing one digit. With this info, we now know how many pixels are needed to move our strip of digits the required distance.

Demo and Code

A runnable demo.

The full source code.

An Animation Enhancement…

Just for fun, the above source code also contains an alternative set of animation keyframes:

JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
digit.animate([
  { offset: 0.05, transform: 'translateY(' + (height * -1) + 'px)' },
  { offset: 0.10, transform: 'translateY(' + (height * -1) + 'px)' },
  { offset: 0.15, transform: 'translateY(' + (height * -2) + 'px)' },
  { offset: 0.20, transform: 'translateY(' + (height * -2) + 'px)' },
  { offset: 0.25, transform: 'translateY(' + (height * -3) + 'px)' },
  { offset: 0.30, transform: 'translateY(' + (height * -3) + 'px)' },
  { offset: 0.35, transform: 'translateY(' + (height * -4) + 'px)' },
  { offset: 0.40, transform: 'translateY(' + (height * -4) + 'px)' },
  { offset: 0.45, transform: 'translateY(' + (height * -5) + 'px)' },
  { offset: 0.50, transform: 'translateY(' + (height * -5) + 'px)' },
  { offset: 0.55, transform: 'translateY(' + (height * -6) + 'px)' },
  { offset: 0.60, transform: 'translateY(' + (height * -6) + 'px)' },
  { offset: 0.65, transform: 'translateY(' + (height * -7) + 'px)' },
  { offset: 0.70, transform: 'translateY(' + (height * -7) + 'px)' },
  { offset: 0.75, transform: 'translateY(' + (height * -8) + 'px)' },
  { offset: 0.80, transform: 'translateY(' + (height * -8) + 'px)' },
  { offset: 0.85, transform: 'translateY(' + (height * -9) + 'px)' },
  { offset: 0.90, transform: 'translateY(' + (height * -9) + 'px)' },
  { offset: 0.95, transform: 'translateY(' + (height * -10) + 'px)' },
  { offset: 1.00, transform: 'translateY(' + (height * -10) + 'px)' },
  { offset: 1, transform: 'translateY(0)' }],
  {
    duration: 5000,
    iterations: 3
  }
);

Here we can see many more keyframes, spread out across the timeline (from 0 to 1, using increments of 0.05). This gives a jump effect where there is a pause between the display of each digit, as opposed to the smooth scroll used in the earlier code.

This approach uses the same visual “trick” of instantaneously jumping to the start of the number strip in the final keyframe.

Simulating an Odometer

Finally, here are all of the above techniques, extended to show an odometer-style display, but using the more involved jump-style, rather than a smooth turning of the dial:

A runnable demo.

The full source code.