
How to Style Numbers and Data Tables with Tabular Figures?
- Sajjad
- Typography
- 01 Oct, 2026
You build a pricing comparison table, right-align the price column, and it still looks ragged. $1,111.00 is visibly narrower than $8,888.00, the decimal points drift a pixel or two from row to row, and a live countdown timer elsewhere on the page jitters left and right every second as the digits change. The data is correct, but the page feels sloppy. The cause is that most modern fonts default to proportional figures, where each digit has its own width, and nobody told the browser to switch.
This article explains the four kinds of numerals fonts can contain, when each is appropriate, and how to control them with font-variant-numeric. Then it covers the practical side of styling data tables: alignment, decimal handling, units, negative numbers, zebra striping, responsive behavior, and accessible markup.
What Are Tabular Figures?
Tabular figures (also called monospaced or fixed-width numerals) are digits that all share the same advance width. A 1 takes as much horizontal space as an 8, so numbers with the same number of digits always line up vertically in a column.
Proportional figures are the opposite: each digit gets a width that suits its shape. The 1 is narrow and the 0 is wide. That looks more natural in running text, where numbers sit inside sentences, but it breaks alignment in columns.
Fonts also distinguish between two styles of figure height:
| Figure style | Description | Best used in |
|---|---|---|
| Lining | All digits sit on the baseline and reach cap height | UI, tables, headings, all-caps text |
| Oldstyle | Digits have ascenders and descenders like lowercase letters | Long-form prose, literary typography |
| Tabular | Every digit has the same width | Tables, prices, timers, dashboards |
| Proportional | Each digit has its natural width | Running text, headlines |
Width and height are independent, so a font can offer four combinations: tabular lining, proportional lining, tabular oldstyle, and proportional oldstyle. Most UI fonts default to proportional lining figures. Fonts like Inter, Roboto, IBM Plex Sans, Source Sans 3, and Lato all include tabular alternates, which you switch on with OpenType features.
You can check what a font supports on its Google Fonts specimen page, in a tool like Wakamai Fondue, or by testing the CSS below and looking for a change.
Turn On Tabular Figures with CSS
The modern, readable way is font-variant-numeric:
.data-table td,
.price,
.timer {
font-variant-numeric: tabular-nums;
}
It accepts several keywords that can be combined:
| Value | OpenType feature | Effect |
|---|---|---|
tabular-nums | tnum | Fixed-width digits |
proportional-nums | pnum | Natural-width digits |
lining-nums | lnum | Cap-height digits on the baseline |
oldstyle-nums | onum | Lowercase-style digits |
slashed-zero | zero | Zero with a slash to distinguish it from O |
diagonal-fractions | frac | 1/2 rendered as a true fraction |
stacked-fractions | afrc | Fraction with a horizontal bar |
ordinal | ordn | Superscript ordinals like 1st, 2nd |
A typical table setting combines several:
.data-table td {
font-variant-numeric: tabular-nums lining-nums slashed-zero;
}
Why Not font-feature-settings?
You can achieve the same result with font-feature-settings: "tnum" 1, and that low-level property is covered in detail in the font-feature-settings guide. The problem is that font-feature-settings doesn't cascade additively. If a parent sets "liga" 0 and a child sets "tnum" 1, the child loses the ligature setting. font-variant-numeric composes cleanly with other font-variant-* properties, so prefer it whenever a high-level property exists.
Tailwind CSS v4
Tailwind ships utilities that map directly to font-variant-numeric:
<td class="tabular-nums lining-nums slashed-zero text-right">1,024.50</td>
These compose, so tabular-nums slashed-zero applies both values.
Where to Use Tabular vs. Proportional Figures
The rule is simple: if numbers need to align vertically or change in place, use tabular figures. If they sit in prose, use proportional.
Use tabular figures for:
- Table columns of prices, quantities, percentages, or dates
- Financial statements and invoices
- Timers, countdowns, stopwatches, and live counters
- Dashboards and stat tiles that update
- Step counters in forms and progress indicators that animate
- Versions and IDs listed in a column
Use proportional figures for:
- Numbers inside body text ("The study covered 1,214 users across 17 countries")
- Large display numbers in a hero, where tabular spacing looks gappy
- Headlines and marketing copy
The timer case is worth stressing. With proportional figures, every tick changes the total width of the string, which shifts every character after it. Tabular figures eliminate that jitter completely:
.countdown {
font-variant-numeric: tabular-nums;
}
What About Oldstyle Figures?
Oldstyle figures blend into lowercase text, which makes them pleasant in long-form editorial content where numbers appear mid-sentence. They're a poor fit for tables, phone numbers, and UI, because the varying heights make digits harder to compare quickly. If your body font offers them, enable them only on article prose:
.prose p {
font-variant-numeric: oldstyle-nums proportional-nums;
}
.prose table {
font-variant-numeric: lining-nums tabular-nums;
}
Align Numbers in Table Columns
Tabular figures solve width; alignment solves position. Numbers in a column should be right-aligned so that units, tens, and hundreds line up. Text columns stay left-aligned.
.data-table {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums lining-nums;
}
.data-table th,
.data-table td {
padding: 0.625rem 0.75rem;
text-align: start;
vertical-align: top;
}
.data-table .num {
text-align: end;
white-space: nowrap;
}
Align the header of a numeric column to match its data. A left-aligned "Revenue" header over right-aligned numbers looks broken.
Using start and end instead of left and right means the table flips correctly in right-to-left languages, which matters if you follow the patterns in RTL typography in CSS.
Aligning Decimals
Right alignment lines up decimals only when every number has the same number of decimal places. The simplest fix is in the data: format every value in a column to the same precision.
const fmt = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
fmt.format(1111); // "$1,111.00"
fmt.format(8.5); // "$8.50"
When precision genuinely varies, such as scientific values, you can't rely on right alignment. CSS once specified text-align: "." for character alignment, but no browser implements it. The practical workaround is splitting the integer and fractional parts into separate cells or spans:
<td class="num">
<span class="int">1,204</span><span class="frac">.5</span>
</td>
.num .frac {
display: inline-block;
min-width: 3ch;
text-align: start;
}
With tabular figures, ch units correspond to the digit width in most fonts, so reserving 3ch for the fractional part keeps the decimal point in a fixed position.
Format Numbers for Readability
Alignment won't help if the numbers themselves are hard to parse. Let Intl.NumberFormat handle locale rules rather than writing your own.
- Thousands separators: Always group digits in numbers of five digits or more. Locales differ:
1,234.5in the US,1.234,5in Germany,1 234,5in France. - Units: Put units in the column header ("Size (MB)") rather than repeating them in every cell. It reduces noise and keeps numbers aligned.
- Negative numbers: Use a true minus sign (U+2212) rather than a hyphen. It's the same width as a plus sign in most fonts and aligns properly. Accounting tables often use parentheses instead.
- Abbreviations: For dashboards, compact notation is far easier to scan.
new Intl.NumberFormat("en-US", { notation: "compact" }).format(1_250_000);
// "1.3M"
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
currencySign: "accounting",
}).format(-420);
// "($420.00)"
Don't rely on red alone to mark negative values. Combine color with a minus sign or parentheses so the meaning survives for colorblind readers and in print, in line with WCAG 1.4.1 (use of color).
Style the Table for Scanning
Readers rarely read tables top to bottom. They scan for a row, then read across. Your styling should support that.
.data-table thead th {
font-weight: 600;
font-size: 0.875rem;
border-bottom: 2px solid var(--color-border-strong);
position: sticky;
top: 0;
background-color: var(--color-body);
}
.data-table tbody tr {
border-bottom: 1px solid var(--color-border);
}
.data-table tbody tr:nth-child(even) {
background-color: var(--color-row-alt);
}
.data-table tbody tr:hover {
background-color: var(--color-row-hover);
}
.data-table tfoot td {
font-weight: 600;
border-top: 2px solid var(--color-border-strong);
}
Practical guidelines:
- Use subtle zebra striping or thin row rules, not both heavily. Either one guides the eye across a row; stacking strong versions of both creates noise.
- Minimize vertical rules. Whitespace and alignment separate columns better than lines.
- Make totals distinct with weight and a heavier top border, placed in a
tfoot. - Keep body size at or near the body text size. Tiny 12px table text is a common mistake. 14px to 16px is readable for dense data.
- Make headers sticky on long tables so the column meaning stays visible.
Weight changes in numbers can also break alignment if bold digits are wider than regular ones. Most well-made fonts keep tabular figures the same width across weights, but test it. Variable fonts with a wght axis usually maintain consistent widths.
Make Tables Accessible
Visual alignment doesn't help screen reader users. Semantic markup does.
<table class="data-table">
<caption>Monthly revenue by plan, 2026</caption>
<thead>
<tr>
<th scope="col">Plan</th>
<th scope="col" class="num">Subscribers</th>
<th scope="col" class="num">Revenue (USD)</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Starter</th>
<td class="num">1,204</td>
<td class="num">14,448.00</td>
</tr>
<tr>
<th scope="row">Pro</th>
<td class="num">386</td>
<td class="num">23,160.00</td>
</tr>
</tbody>
</table>
The caption names the table, scope connects cells to their headers, and row headers let a screen reader announce "Pro, Revenue, 23,160.00" instead of a bare number. Keep text contrast at 4.5:1 per WCAG 1.4.3, including for striped rows, where the stripe color lowers contrast slightly.
Handle Tables on Small Screens
Wide data tables don't fit on phones. The most robust pattern is a horizontally scrollable wrapper that is keyboard-focusable and labeled:
<div class="table-wrap" role="region" aria-label="Monthly revenue" tabindex="0">
<table class="data-table">...</table>
</div>
.table-wrap {
overflow-x: auto;
}
.table-wrap:focus-visible {
outline: 2px solid var(--color-focus);
}
.data-table th:first-child,
.data-table td:first-child {
position: sticky;
inset-inline-start: 0;
background-color: var(--color-body);
}
A sticky first column keeps the row label visible while the reader scrolls through numbers. Reflowing each row into a stacked card works for simple tables but destroys the ability to compare values down a column, which is usually the whole point of a data table.
Tabular Figures FAQ
Tabular figures give every digit the same width so numbers line up in columns and don't shift when they change. Proportional figures give each digit a natural width, which looks better in sentences but causes ragged columns and jittery counters.
No. The font must include the tnum OpenType feature. Most professional UI fonts such as Inter, Roboto, IBM Plex Sans, and Source Sans 3 do. If a font lacks it, the property has no effect, and you can either switch fonts for numeric columns or use a monospace font there.
You can, but it changes the whole character of the text, including letters and punctuation. Tabular figures in your existing UI font keep the visual style consistent while giving you fixed-width digits, which is usually the better choice.
No. The alternate digits are already in the font file if the font supports them. Enabling the feature just tells the browser which glyphs to use. Be careful when subsetting fonts, though, because aggressive subsetting can strip OpenType features unless you explicitly keep them.
Format every value in the column to the same number of decimal places and right-align the column with tabular figures. When precision has to vary, split the integer and fractional parts into separate spans and give the fractional part a fixed minimum width in ch units.
Usually not. Large display numbers look better with proportional spacing because tabular spacing leaves visible gaps around narrow digits like 1. The exception is a big number that animates or updates, such as a live counter, where stability matters more than spacing.
Conclusion
Numbers are some of the most important content on a page, and a single CSS declaration does most of the work of presenting them well. Turn on tabular-nums anywhere numbers align in columns or change in place, keep proportional figures for prose and display sizes, and choose lining figures for UI and tables.
Then let alignment and formatting finish the job: right-align numeric columns and their headers, format values to consistent precision with Intl.NumberFormat, use true minus signs, move units into headers, and mark up tables with captions and scoped headers. Your tables will be faster to scan, easier to trust, and accessible to everyone.
Here are some useful references for going deeper on tabular figures and data tables:
- MDN Web Docs: font-variant-numeric — every value and its OpenType equivalent.
- MDN Web Docs: Intl.NumberFormat — locale-aware number, currency, and compact formatting.
- W3C WAI: Tables Tutorial — accessible table markup with captions and header associations.
- Microsoft Typography: OpenType Layout tag registry: features — definitions of tnum, pnum, lnum, onum, and zero.
- Butterick's Practical Typography: Tables — guidance on minimal rules, alignment, and table design.


