Last month I was building a stopwatch component for a project dashboard. The numbers were doing the dance — every time the seconds ticked over, the whole layout shifted by a pixel or two. Looked janky. The fix was one line of CSS I'd been ignoring for years: font-variant-numeric: tabular-nums.
Let me share what I learned migrating a few projects off the old monospace hack.
The Problem
Proportional fonts (which is almost every font you use) give different widths to different digits. A "1" is narrow, an "8" is wide. So when a number flips from 11:11 to 12:23, the whole string can shift horizontally. For static text that's fine. For timers, leaderboards, financial tables, live prices — it's terrible UX.
The classic fixes:
- Switch to a monospace font (ugly, often the wrong vibe)
- Pad with
text-align: rightand pray - Use JS to set fixed widths
- Make the whole UI in Helvetica from 1998
There's a better way that's been in CSS for years.
The Three Approaches
1. Monospace fonts
.timer {
font-family: "JetBrains Mono", "SF Mono", monospace;
}
This works. Every character has the same width. But you're paying for it stylistically — your dashboard now looks like a terminal. Monospace fonts also tend to ship with wider character widths than proportional ones, so your layout has to accommodate.
2. font-variant-numeric (the modern fix)
.timer {
/* Only the digits get equal widths — letters stay proportional */
font-variant-numeric: tabular-nums;
}
This is what you actually want most of the time. Most modern fonts (Inter, Roboto, system fonts on macOS/Windows) ship with both proportional and tabular figure variants. The tabular-nums value tells the font to use the tabular set. Letters stay proportional, digits become uniform.
3. font-feature-settings (the low-level version)
.timer {
font-feature-settings: "tnum" 1;
}
Same outcome, older API. It directly toggles the OpenType tnum feature. MDN recommends the higher-level font-variant-numeric property when both work, because it composes better with other CSS rules and doesn't accidentally clobber unrelated font features.
Side-by-Side
After migrating three different projects, here's how the approaches stack up:
| Approach | Pros | Cons |
|---|---|---|
| Monospace font | Always works, no font-feature dependency | Changes the whole visual feel; wider columns |
font-variant-numeric: tabular-nums |
Keeps your design font, semantic API | Requires font with tabular figure support |
font-feature-settings: "tnum" |
Same effect, very wide support | Low-level, can clobber other features |
The font-support thing is real but smaller than you'd think. Inter, Roboto, Source Sans, IBM Plex, system-ui on Apple, Segoe UI on Windows — they all support tnum. If it's a Google Font, you can usually toggle it on in the embed URL.
Migrating From Monospace to tabular-nums
Here's a before/after from a leaderboard component I cleaned up last week.
Before:
<div class="leaderboard">
<span class="player">jdoe</span>
<!-- Forced monospace just to stop the score column jittering -->
<span class="score mono">1,247</span>
</div>
.score.mono {
font-family: "Menlo", monospace;
font-size: 14px;
/* Bumped font-size down to match visual weight of the body font */
}
After:
<div class="leaderboard">
<span class="player">jdoe</span>
<span class="score">1,247</span>
</div>
.score {
font-variant-numeric: tabular-nums;
/* Inherits the design system font — no override needed */
}
The migration path is pretty mechanical:
- Search your stylesheets for
font-family.*mono— anywhere you forced monospace just to align numbers - Replace the
font-familyoverride withfont-variant-numeric: tabular-nums - Visually verify the font actually supports tabular figures (some don't — you'll see no change)
- Remove the size adjustments you probably added to compensate for monospace metrics
One gotcha: if you use Tailwind's tabular-nums utility, that's just the same CSS property under the hood — no magic, same font-support caveats.
Combining Numeric Features
font-variant-numeric accepts several values that can be combined:
.financial-table {
/* Tabular figures + a slashed zero — easier to distinguish 0 from O */
font-variant-numeric: tabular-nums slashed-zero;
}
.recipe {
/* Real typographic fractions instead of flat "1/2" */
font-variant-numeric: diagonal-fractions;
}
I haven't tested diagonal-fractions across many fonts thoroughly; it's font-dependent and a lot of faces don't include the glyphs. But tabular-nums slashed-zero is now my default in any data-heavy UI.
When to Use What
My rule of thumb after three migrations:
-
Any updating number (timers, counters, live scores, live prices):
tabular-nums -
Financial tables, spreadsheet-like UIs:
tabular-nums slashed-zero - Code, terminal output, diffs: actual monospace — the whole point is that everything aligns, not just digits
- Static prose with numbers in it (article body, marketing copy): nothing. Proportional figures look better in running text
The mistake I made for years was reaching for monospace by default. Most of the time you don't want a code font; you want the numbers in your existing font to behave.
Browser Support
font-variant-numeric: tabular-nums works in all modern browsers and has for a long while. Per caniuse global support is above 96%. The bigger support question isn't the browser — it's whether your font includes the feature. A font without tnum glyphs will silently do nothing, and that's the failure mode I see people hit most often.
The Short Version
If you're using a monospace font purely to stop digits from jittering, you almost certainly want font-variant-numeric: tabular-nums instead. One property, no JS, no font swap, no design tradeoff. It's the small kind of thing that quietly upgrades every dashboard you ship going forward.
Top comments (0)