Guides

Why my numbers jump when they change — the timer jitters and the text next to it moves

Illustration: a scoreboard of uneven plates next to a neat, even grid

You built a countdown timer. The digits change honestly: 0:19, 0:18, 0:17… And the whole line trembles. The button on the right shifts back and forth by a couple of pixels. Every second.

The code is fine. It's the font, and here's the surprising part: in most modern fonts a 1 is narrower than a 0. "1:11" is physically shorter than "0:00". When a digit changes, the line's width changes — and everything around it moves.

Below are three causes, most common first. Each with how to check and how to fix.

Cause 1. Proportional digits

Type designers draw digits in two ways:

  • proportional — each has its own width, like letters. "1" is narrow, "0" is wide. Looks good in prose: "in 1990" doesn't look gappy;
  • tabular — all digits share one width, like cells. Duller in prose, but columns and timers stand perfectly still.

Many fonts default to proportional. So does Apple's system font, which is why timers jitter especially on iPhones.

How to check. Put two blocks side by side: <span>111111</span> and <span>000000</span>. Different lengths — your digits are proportional.

How to fix. One line of CSS:

.timer {
  font-variant-numeric: tabular-nums;
}

It asks the font for its tabular digit set. The font switches, and every digit gets the same width. Letters stay untouched — only digits.

Use it wherever a number changes in front of the user or sits in a column: timers, counters, prices in a table, upload percentages, ratings, balances. Not in article prose — proportional digits read better there.

In Tailwind it's the tabular-nums class. In SwiftUI it's the .monospacedDigit() modifier. Changed the style and nothing happened? First check "Why my CSS isn't working" — the rule may simply not apply.

Cause 2. The number of digits changes

You added tabular-nums, yet it still jumps. But only sometimes: at 9 → 10, 99 → 100, 59:59 → 1:00:00.

Tabular digits have equal width. But two digits are still wider than one. No font can fix that.

How to check. Does it jump only when the number "rolls over" to a new digit? That's it.

How to fix. Three options, pick by meaning:

  • Leading zeros. Time is written "05:07", not "5:7". In JS: String(sec).padStart(2, "0"). Constant width, and it reads naturally.
  • A minimum width in ch. The ch unit is the width of "0" in the current font. With tabular digits it equals the width of any digit:
.counter {
  display: inline-block; /* a plain span ignores width */
  font-variant-numeric: tabular-nums;
  min-width: 4ch;
  text-align: right;
}

Now the counter keeps its size up to 9999.

  • Right alignment. If the width does grow, let it grow to the left. A centered number expands both ways and jitters the most.

Cause 3. The font has no tabular digits

The sneakiest one. You did everything right, and tabular-nums had no effect. No errors, no warnings.

The property only asks the font. If the font has no tabular set, the browser silently leaves things as they were. This happens with decorative and handwritten fonts, some free ones, and fonts stripped of extra features to save weight.

How to check. Cause 1's test, now with tabular-nums applied: if "111111" and "000000" still differ in length, the font has no tabular digits. In DevTools, the bottom of the Computed tab shows which font actually rendered the text. Sometimes it's a fallback, not the one you loaded.

How to fix.

  • Use a different font for numbers — one with tabular digits. Font pages usually list it as tnum. Many variable fonts support it.
  • Last resort — a monospace font just for digits: font-family: ui-monospace, monospace. It looks more technical, but the jumping stops for sure.

A checklist for any live number

Before showing users a changing number, run through this list:

  1. font-variant-numeric: tabular-nums is on the element holding the number.
  2. The "111111 vs 000000" test passes — same length.
  3. The width fits the longest value: min-width in ch or leading zeros.
  4. The number is right-aligned, not centered.
  5. Tested on an iPhone — its system font has proportional digits.

Five items — and the timer stays glued in place.

Can I just set a fixed width in pixels?

You can, but it's fragile. The font size changes on a phone, or the user turns on large text — and the number spills out of its box. The ch unit grows with the font; pixels don't.

What if the number animates, like a "0 → 1,000,000" counter?

Then everything jumps at once: digit widths, digit count, and the thousands separator. Use tabular-nums, a min-width in ch sized for the final number, and right alignment. The animation then runs in place.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app
KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →