5 Pure CSS2 with JSPublished 2026-10-05Free · no sign-up
Seven table designs for real data: striped rows with a hover highlight, a table that turns into cards on narrow screens, sortable columns with aria-sort, a financial table with totals, a minimal spec sheet, rows with status pills and actions and a comfortable and compact density switch.
A clean data table with a caption, column headers with scope, alternating row shading and a hover highlight. Striping uses :nth-child(even) on body rows only, and the wrapper scrolls sideways on small screens rather than squashing the columns.
Below 460px of container width each row turns into a card and each cell shows its column name from a data-label attribute. Changing display on table elements can remove their table meaning in some browsers, so explicit ARIA table roles keep it.
Clicking a column header sorts the table. The header contains a button, so it is keyboard operable, and the header cell gets aria-sort set to ascending or descending, which screen readers announce along with the column name.
var ths = document.querySelectorAll('.tbl3 th'), tb = document.querySelector('.tbl3 tbody');
ths.forEach(function (th) { th.querySelector('button').onclick = function () {
var k = +this.dataset.k, asc = th.getAttribute('aria-sort') !== 'ascending';
ths.forEach(function (x) { x.removeAttribute('aria-sort'); }); th.setAttribute('aria-sort', asc ? 'ascending' : 'descending');
Array.prototype.slice.call(tb.rows).sort(function (a, b) { var x = a.cells[k].textContent, y = b.cells[k].textContent; var c = k ? x - y : x.localeCompare(y); return asc ? c : -c; }).forEach(function (r) { tb.appendChild(r); });
}; });
04. Financial Table with Totals
TableNumbers
Numbers right-aligned with tabular figures so the digits line up by place value, negative values in brackets and colour, and a totals row in tfoot. The totals row uses th scope="row" for its label so it reads as Total, $12,420.
A table with no vertical lines and only hairline row dividers, generous padding and muted headers, a style that suits editorial and spec sheets. Less ink on lines leaves more attention for the data.
Rows with a status pill and an Edit action at the end of each row. The action buttons name their row (Edit invoice 2041), because a column of identical Edit buttons is meaningless when read on its own.
A toggle switches the table between comfortable and compact row heights by changing one custom property for the cell padding. Power users scanning long lists prefer compact; most people prefer comfortable, so it is the default.
var w = document.querySelector('.tbl7'), bs = w.querySelectorAll('.t button');
bs.forEach(function (b) { b.onclick = function () { bs.forEach(function (x) { x.setAttribute('aria-pressed', x === b); }); w.style.setProperty('--pad', b.dataset.p); }; });
About these CSS Table Styles
Real tables for tabular data
Use <table> with a <caption> and th cells with scope. Screen readers then announce the column and row headers for each cell, which no grid of divs can do.
Numbers line up by place
Right-align numeric columns and set font-variant-numeric: tabular-nums so every digit has the same width and values can be compared at a glance.
Scroll, or restructure, on small screens
Wrap wide tables in a focusable region with overflow-x: auto, or turn rows into cards with data-label attributes. If you change table display values, add ARIA table roles so the semantics survive.
Sorting is announced with aria-sort
Put a button inside each sortable header and set aria-sort on the header cell to ascending or descending. Only one column should carry it at a time.
Frequently asked questions
Should tables have zebra stripes?
They help on wide tables with many columns; on narrow ones, row dividers are enough.
How do I label an action column?
Give the empty header a visually hidden Actions label and name each button after its row.
Can I make the header sticky?
Yes. See the sticky elements collection for a sticky table header.