SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/table-styles

7 CSS Table Styles

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.

01. Striped Table with Row Hover

TableStripes

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.

View code
<div class="tbl1" tabindex="0" role="region" aria-labelledby="tbl1c"><table><caption id="tbl1c">Team members</caption><thead><tr><th scope="col">Name</th><th scope="col">Role</th><th scope="col">Location</th></tr></thead><tbody><tr><td>Ana Ribeiro</td><td>Design lead</td><td>Lisbon</td></tr><tr><td>Jonas Weber</td><td>Engineer</td><td>Leipzig</td></tr><tr><td>Mei Tanaka</td><td>Data</td><td>Tokyo</td></tr><tr><td>Olu Bakare</td><td>Support</td><td>Lagos</td></tr></tbody></table></div>
.tbl1 { position: relative; width: 420px; max-width: 100%; overflow-x: auto; box-sizing: border-box; border: 1px solid #e4e7ec; border-radius: 12px; background: #fff; font: 14px Inter, system-ui, sans-serif; }
.tbl1:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.tbl1 table { width: 100%; border-collapse: collapse; }
.tbl1 caption { padding: 12px 14px; color: #101828; font-weight: 800; text-align: left; }
.tbl1 th, .tbl1 td { padding: 10px 14px; text-align: left; white-space: nowrap; }
.tbl1 thead th { background: #f9fafb; color: #475467; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; border-top: 1px solid #e4e7ec; border-bottom: 1px solid #e4e7ec; }
.tbl1 tbody tr:nth-child(even) { background: #f9fafb; }
.tbl1 tbody tr:hover { background: #eef4ff; }
.tbl1 td { color: #344054; }

02. Table That Becomes Cards on Mobile

TableResponsive

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.

View code
<div class="tbl2"><table role="table"><thead role="rowgroup"><tr role="row"><th role="columnheader" scope="col">Order</th><th role="columnheader" scope="col">Date</th><th role="columnheader" scope="col">Total</th><th role="columnheader" scope="col">Status</th></tr></thead><tbody role="rowgroup"><tr role="row"><td role="cell" data-label="Order">#1042</td><td role="cell" data-label="Date">3 Oct</td><td role="cell" data-label="Total">$84.00</td><td role="cell" data-label="Status">Shipped</td></tr><tr role="row"><td role="cell" data-label="Order">#1041</td><td role="cell" data-label="Date">1 Oct</td><td role="cell" data-label="Total">$120.00</td><td role="cell" data-label="Status">Delivered</td></tr></tbody></table></div>
.tbl2 { width: 520px; max-width: 100%; container-type: inline-size; font: 14px Inter, system-ui, sans-serif; }
.tbl2 table { width: 100%; border-collapse: collapse; border-radius: 12px; overflow: hidden; background: #111727; }
.tbl2 th, .tbl2 td { padding: 10px 12px; text-align: left; color: #e2e8f0; border-bottom: 1px solid #263048; }
.tbl2 th { color: #94a3b8; font-size: 12px; }
@container (max-width: 459px) {
  .tbl2 table, .tbl2 tbody, .tbl2 tr, .tbl2 td { display: block; background: none; }
  .tbl2 thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .tbl2 tr { margin-bottom: 10px; padding: 6px 12px; border-radius: 12px; background: #151b2e; }
  .tbl2 td { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #263048; }
  .tbl2 td:last-child { border-bottom: 0; }
  .tbl2 td::before { content: attr(data-label); color: #94a3b8; font-size: 12px; font-weight: 600; }
}

03. Sortable Columns with aria-sort

TableSorting

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.

View code
<div class="tbl3"><table><caption>Products, sortable</caption><thead><tr><th scope="col" aria-sort="ascending"><button data-k="0">Name</button></th><th scope="col"><button data-k="1">Price</button></th><th scope="col"><button data-k="2">Stock</button></th></tr></thead><tbody><tr><td>Basket</td><td>26</td><td>12</td></tr><tr><td>Candle</td><td>18</td><td>40</td></tr><tr><td>Mug</td><td>14</td><td>7</td></tr><tr><td>Throw</td><td>89</td><td>3</td></tr></tbody></table></div>
.tbl3 { width: 360px; max-width: 100%; font: 14px Inter, system-ui, sans-serif; }
.tbl3 table { width: 100%; border-collapse: collapse; }
.tbl3 caption { margin-bottom: 8px; color: #101828; font-weight: 800; text-align: left; }
.tbl3 th { padding: 0; border-bottom: 2px solid #101828; text-align: left; }
.tbl3 th button { display: flex; align-items: center; gap: 6px; width: 100%; padding: 10px 8px; border: 0; background: none; color: #101828; font: 700 13px Inter, system-ui, sans-serif; cursor: pointer; }
.tbl3 th button::after { content: "↕"; color: #98a2b3; font-size: 12px; }
.tbl3 th[aria-sort=ascending] button::after { content: "↑"; color: #101828; } .tbl3 th[aria-sort=descending] button::after { content: "↓"; color: #101828; }
.tbl3 td { padding: 10px 8px; border-bottom: 1px solid #e4e7ec; color: #344054; }
.tbl3 td:not(:first-child), .tbl3 th:not(:first-child) button { text-align: right; justify-content: flex-end; }
.tbl3 button:focus-visible { outline: 3px solid #84adff; outline-offset: -3px; }
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.

View code
<div class="tbl4"><table><caption>Q3 summary</caption><thead><tr><th scope="col">Month</th><th scope="col">Revenue</th><th scope="col">Change</th></tr></thead><tbody><tr><td>July</td><td>$3,980</td><td class="up">+4.2%</td></tr><tr><td>August</td><td>$4,120</td><td class="up">+3.5%</td></tr><tr><td>September</td><td>$4,320</td><td class="dn">(1.1%)</td></tr></tbody><tfoot><tr><th scope="row">Total</th><td>$12,420</td><td></td></tr></tfoot></table></div>
.tbl4 { width: 340px; max-width: 100%; font: 14px Inter, system-ui, sans-serif; }
.tbl4 table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.tbl4 caption { margin-bottom: 8px; color: #fff; font-weight: 800; text-align: left; }
.tbl4 th, .tbl4 td { padding: 9px 8px; border-bottom: 1px solid #263048; color: #e2e8f0; text-align: right; }
.tbl4 th:first-child, .tbl4 td:first-child { text-align: left; }
.tbl4 thead th { color: #94a3b8; font-size: 12px; font-weight: 600; }
.tbl4 .up { color: #4ade80; } .tbl4 .dn { color: #fbbf24; }
.tbl4 tfoot th, .tbl4 tfoot td { border-top: 2px solid #94a3b8; border-bottom: 0; color: #fff; font-weight: 800; }

05. Minimal Borderless Table

TableMinimal

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.

View code
<table class="tbl5"><caption>Specifications</caption><tbody><tr><th scope="row">Weight</th><td>1.2 kg</td></tr><tr><th scope="row">Battery</th><td>14 hours</td></tr><tr><th scope="row">Screen</th><td>13.6 in, 2560 × 1664</td></tr><tr><th scope="row">Ports</th><td>2 × USB-C, headphone</td></tr></tbody></table>
.tbl5 { width: 340px; max-width: 100%; border-collapse: collapse; font: 14px Inter, system-ui, sans-serif; }
.tbl5 caption { padding-bottom: 10px; color: #101828; font-weight: 800; text-align: left; }
.tbl5 th, .tbl5 td { padding: 12px 0; border-top: 1px solid #eaecf0; text-align: left; vertical-align: top; }
.tbl5 th { width: 40%; color: #475467; font-weight: 500; } .tbl5 td { color: #101828; font-weight: 600; }

06. Table with Status and Row Actions

TableActions

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.

View code
<div class="tbl6" tabindex="0" role="region" aria-labelledby="tbl6c"><table><caption id="tbl6c">Invoices</caption><thead><tr><th scope="col">Invoice</th><th scope="col">Status</th><th scope="col"><span class="sr">Actions</span></th></tr></thead><tbody><tr><td>#2041</td><td><span class="p ok">Paid</span></td><td><button aria-label="Edit invoice 2041">Edit</button></td></tr><tr><td>#2040</td><td><span class="p due">Overdue</span></td><td><button aria-label="Edit invoice 2040">Edit</button></td></tr></tbody></table></div>
.tbl6 { position: relative; width: 340px; max-width: 100%; overflow-x: auto; font: 14px Inter, system-ui, sans-serif; }
.tbl6:focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }
.tbl6 table { width: 100%; border-collapse: collapse; }
.tbl6 caption { margin-bottom: 8px; color: #fff; font-weight: 800; text-align: left; }
.tbl6 th, .tbl6 td { padding: 10px 8px; border-bottom: 1px solid #263048; color: #e2e8f0; text-align: left; white-space: nowrap; }
.tbl6 th { color: #94a3b8; font-size: 12px; } .tbl6 td:last-child { text-align: right; }
.tbl6 .p { padding: 3px 8px; border-radius: 999px; font-size: 12px; font-weight: 700; } .tbl6 .ok { background: #052e16; color: #86efac; } .tbl6 .due { background: #422006; color: #fcd34d; }
.tbl6 button { padding: 5px 10px; border: 1px solid #3a4670; border-radius: 6px; background: transparent; color: #c7d2fe; font: 600 12px Inter, system-ui, sans-serif; cursor: pointer; }
.tbl6 button:focus-visible { outline: 2px solid #a5b4fc; outline-offset: 1px; }
.tbl6 .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

07. Comfortable and Compact Density

TableDensity

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.

View code
<div class="tbl7"><div class="t" role="group" aria-label="Row density"><button aria-pressed="true" data-p="12px">Comfortable</button><button aria-pressed="false" data-p="5px">Compact</button></div><table><thead><tr><th scope="col">File</th><th scope="col">Size</th></tr></thead><tbody><tr><td>brief.pdf</td><td>2.4 MB</td></tr><tr><td>logo.svg</td><td>18 KB</td></tr><tr><td>photos.zip</td><td>96 MB</td></tr></tbody></table></div>
.tbl7 { --pad: 12px; width: 320px; max-width: 100%; font: 14px Inter, system-ui, sans-serif; }
.tbl7 .t { display: flex; gap: 4px; margin-bottom: 10px; }
.tbl7 .t button { padding: 6px 10px; border: 1px solid #d0d5dd; border-radius: 8px; background: #fff; font: 600 12px Inter, system-ui, sans-serif; cursor: pointer; }
.tbl7 .t [aria-pressed=true] { background: #101828; color: #fff; border-color: #101828; }
.tbl7 button:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.tbl7 table { width: 100%; border-collapse: collapse; }
.tbl7 th, .tbl7 td { padding: var(--pad) 10px; border-bottom: 1px solid #e4e7ec; text-align: left; color: #344054; transition: padding .2s; }
.tbl7 th { color: #475467; font-size: 12px; } .tbl7 td:last-child, .tbl7 th:last-child { text-align: right; }
@media (prefers-reduced-motion: reduce) { .tbl7 th, .tbl7 td { transition: none; } }
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.