
How to design readable tables?
- Sajjad
- Typography
- 11 Oct, 2026
To design readable tables, align text to the left and numbers to the right, use tabular numerals so digits line up, give cells enough padding to breathe, and separate rows with subtle horizontal lines or light zebra striping rather than heavy grid borders. Headers should be clearly distinguished with weight or a background and should align the same way as the data beneath them. On small screens, let wide tables scroll horizontally inside a labelled container, or restructure them into stacked cards when the columns are few. Always use real table markup with th elements, scope attributes and a caption, so screen readers can announce the relationships between headers and cells. Good tables let people compare values quickly without thinking about the layout.
Tables are where typography meets data. A pricing comparison, a set of API parameters or a financial report can be scanned in seconds when it's designed well, or become a wall of cramped figures when it isn't. In this article you'll learn the alignment rules that make tables scannable, how to handle numbers, how to choose spacing and borders, how to build sticky headers and responsive tables, and how to mark them up accessibly.
Start With Accessible Markup
Good design starts with correct HTML. A table needs a caption, a header row and header cells that describe their row or column.
<table class="data-table">
<caption>Monthly plan comparison</caption>
<thead>
<tr>
<th scope="col">Plan</th>
<th scope="col" class="num">Users</th>
<th scope="col" class="num">Storage (GB)</th>
<th scope="col" class="num">Price (£/month)</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Starter</th>
<td class="num">3</td>
<td class="num">50</td>
<td class="num">9.00</td>
</tr>
<tr>
<th scope="row">Team</th>
<td class="num">25</td>
<td class="num">500</td>
<td class="num">49.00</td>
</tr>
<tr>
<th scope="row">Business</th>
<td class="num">250</td>
<td class="num">5,000</td>
<td class="num">299.00</td>
</tr>
</tbody>
</table>
What each part does:
- caption: Gives the table a name that screen readers announce when users reach it. You can style it or visually hide it, but keep it in the markup.
- thead and tbody: Group header rows and data rows. They also help with sticky headers and printing, where browsers can repeat
theadon each page. - th with scope: Marks header cells.
scope="col"applies to the column below,scope="row"to the row beside it. - Units in headers: Put units like GB or £ in the header so the cells contain only numbers, which keeps them clean and aligned.
Never use tables for page layout, and don't build data tables out of div elements unless you add the full set of ARIA table roles, which is easy to get wrong.
Alignment Rules
Alignment is the most important factor in how quickly a table can be read.
- Text: Left-align, or more precisely start-align so it works in right-to-left languages. Ragged text is easier to read than centred text in narrow columns.
- Numbers: Right-align so units, tens and hundreds line up and values can be compared by length.
- Decimals: Use the same number of decimal places throughout a column, so right alignment also aligns the decimal points.
- Headers: Align each header with its column's content. A right-aligned number column needs a right-aligned header.
- Short codes and icons: Centring is acceptable for single characters, status icons or check marks, but avoid it for anything with variable length.
- Vertical alignment: Align cells to the top when rows contain multi-line text, so the first lines of each cell line up.
.data-table th,
.data-table td {
text-align: start;
vertical-align: top;
}
.data-table .num {
text-align: end;
}
Make Numbers Line Up With Tabular Numerals
Many fonts use proportional numerals by default, where a 1 is narrower than an 8. That looks good in running text but makes columns of figures wobble. Switch to tabular numerals in tables:
.data-table {
font-variant-numeric: tabular-nums;
}
This only works if the font includes tabular figures, which most good interface fonts do, including Inter, Roboto, Source Sans 3 and IBM Plex Sans. Some fonts also offer lining figures that sit on the baseline at capital height; if your font defaults to old-style figures, add lining-nums too:
.data-table {
font-variant-numeric: tabular-nums lining-nums;
}
You can see the difference by comparing a column of values with and without the setting. With proportional figures, 111.11 is noticeably narrower than 888.88. With tabular figures they are exactly the same width. For more detail, see tabular and proportional numerals.
Formatting Numbers Consistently
Use thousands separators, a consistent number of decimal places and a consistent format for negative values. If you generate tables with JavaScript, Intl.NumberFormat handles this for you:
const gbp = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
minimumFractionDigits: 2,
});
const values = [9, 49, 299, -12.5];
console.log(values.map((v) => gbp.format(v)));
[ '£9.00', '£49.00', '£299.00', '-£12.50' ]
Spacing and Density
Cell padding controls how dense the table feels. Too little and rows blur together; too much and users have to move their eyes further to compare.
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9375rem;
line-height: 1.45;
}
.data-table th,
.data-table td {
padding: 0.625rem 0.875rem;
}
.data-table caption {
caption-side: top;
text-align: start;
padding-block-end: 0.75rem;
font-weight: 600;
}
A few guidelines:
- Horizontal padding: Should be larger than vertical padding, because columns need clearer separation than rows when there are no vertical lines.
- Font size: Tables can be slightly smaller than body text, often 0.875rem to 0.9375rem, but avoid going below about 13px for data people need to read.
- Density options: Data-heavy apps often offer a compact mode. Implement it with a class that reduces padding rather than font size.
.data-table.is-compact th,
.data-table.is-compact td {
padding: 0.375rem 0.625rem;
}
Borders, Lines and Zebra Stripes
Heavy grids with borders on every side add visual noise. In most cases you need far less.
Horizontal Rules Only
Thin lines between rows guide the eye along each row without boxing every cell:
.data-table thead th {
border-block-end: 2px solid #1f2937;
font-weight: 600;
vertical-align: bottom;
}
.data-table tbody tr {
border-block-end: 1px solid #e5e7eb;
}
.data-table tbody tr:last-child {
border-block-end: 2px solid #1f2937;
}
Zebra Striping
Alternating row backgrounds help on wide tables where the eye has to travel far between the first and last columns. Keep the stripe very light so it doesn't compete with text:
.data-table--striped tbody tr:nth-child(even) {
background: #f8fafc;
}
Stripes and row lines do similar jobs, so you rarely need both. Narrow tables with few columns usually read fine with lines alone.
Row Hover and Selection
Hover highlighting helps users keep their place in a long row, but it isn't available on touch screens, so don't rely on it alone.
.data-table tbody tr:hover {
background: #eef2ff;
}
.data-table tbody tr[aria-selected="true"] {
background: #dbeafe;
box-shadow: inset 3px 0 0 #2563eb;
}
Sticky Headers and First Columns
Long tables are easier to read when the header stays in view. position: sticky works on th elements inside a scrolling container:
.table-scroll {
max-height: 70vh;
overflow: auto;
}
.table-scroll thead th {
position: sticky;
top: 0;
z-index: 1;
background: #ffffff;
}
.table-scroll tbody th[scope="row"] {
position: sticky;
inset-inline-start: 0;
background: #ffffff;
}
Sticky cells need a background, or the content scrolling underneath will show through.
Responsive Tables
Wide tables are the hardest part of responsive design. There are two main approaches.
Horizontal Scrolling
For tables with many columns, let them scroll inside a container. Make the container keyboard-focusable and labelled, so keyboard users can scroll it and screen reader users know what it is:
<div class="table-scroll" role="region" aria-labelledby="plans-caption" tabindex="0">
<table class="data-table">
<caption id="plans-caption">Monthly plan comparison</caption>
<!-- ... -->
</table>
</div>
.table-scroll {
overflow-x: auto;
}
.table-scroll:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.table-scroll table {
min-width: 40rem;
}
Stacked Rows on Small Screens
For tables with a few columns, you can stack each row as a card on narrow screens and show the column name next to each value using a data attribute:
<td class="num" data-label="Users">25</td>
@media (max-width: 40em) {
.data-table--stack thead {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.data-table--stack tr {
display: block;
padding-block: 0.75rem;
}
.data-table--stack td,
.data-table--stack tbody th {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.25rem 0;
}
.data-table--stack td::before {
content: attr(data-label);
font-weight: 600;
text-align: start;
}
}
Changing the display of table elements can remove table semantics in some browser and screen reader combinations. If you use this pattern, add explicit ARIA roles (role="table", role="row", role="cell" and so on) and test with a screen reader.
Typography Choices for Tables
- Use a font with clear numerals: Distinguishable 1, l and I, and 0 and O, matter in data.
- Keep weights light on data: Use regular weight for values and semibold for headers. Bolding entire columns adds no information.
- Avoid all caps for long headers: Small uppercase labels can work for short headers if you add a little letter spacing, but long uppercase headers are slow to read.
- Abbreviate carefully: If you shorten headers, use the
abbrelement with atitle, or explain abbreviations in the caption. - Truncate with care: Long text in cells can be truncated with an ellipsis, but make the full value available, for example via a tooltip or detail view.
FAQ: Readable Tables
Yes. Right alignment lines up units, tens and hundreds so values can be compared by length. Use a consistent number of decimal places and tabular numerals so decimal points also line up.
Both help the eye follow a row. Stripes are more useful on wide tables with many columns, while thin row lines are cleaner on narrow tables. You rarely need both at once.
For tables with many columns, wrap them in a focusable container that scrolls horizontally. For tables with a few columns, you can stack each row as a card on small screens, but add ARIA roles and test with a screen reader.
It switches the font to tabular figures, where every digit has the same width. This keeps columns of numbers aligned. It only works if the font includes tabular figures, which most interface fonts do.
A caption is strongly recommended because screen readers announce it and it tells all users what the table contains. If the design already has a visible heading, you can reference it or visually hide the caption.
Slightly smaller than body text works well, often 14px to 15px. Avoid going much below 13px for data people need to read, and reduce padding rather than font size when you need a denser table.
Conclusion
Readable tables depend on a handful of disciplined choices. Start-align text, right-align numbers, use tabular numerals and consistent formatting, and align headers with their columns. Give cells enough horizontal padding, separate rows with light lines or subtle stripes, and keep the header visible on long tables.
Underneath the styling, use real table markup with a caption, th elements and scope attributes, and treat responsive layouts with care so semantics survive on small screens. Tables built this way let readers compare values almost instantly, which is the whole point of putting data in a table.


