DEV Community

Cover image for Tabular Numbers in CSS: font-variant-numeric vs Monospace Hacks
Alan West
Alan West

Posted on

Tabular Numbers in CSS: font-variant-numeric vs Monospace Hacks

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: right and 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
.score.mono {
  font-family: "Menlo", monospace;
  font-size: 14px;
  /* Bumped font-size down to match visual weight of the body font */
}
Enter fullscreen mode Exit fullscreen mode

After:

<div class="leaderboard">
  <span class="player">jdoe</span>
  <span class="score">1,247</span>
</div>
Enter fullscreen mode Exit fullscreen mode
.score {
  font-variant-numeric: tabular-nums;
  /* Inherits the design system font — no override needed */
}
Enter fullscreen mode Exit fullscreen mode

The migration path is pretty mechanical:

  1. Search your stylesheets for font-family.*mono — anywhere you forced monospace just to align numbers
  2. Replace the font-family override with font-variant-numeric: tabular-nums
  3. Visually verify the font actually supports tabular figures (some don't — you'll see no change)
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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)