Table
A plain <table> style for static or simple data. For a real data grid — sorting, filtering, paging, editing — use Tabulator and its adapter; this does not try to replace it.
The table
_78-table on the table, wrapped in _78-table-wrap so it scrolls inside its
own box on a narrow screen instead of stretching the page. The wrapper is not optional in a
responsive layout — without it a wide table pushes the whole document sideways.
| Customer | Plan | Seats | MRR | Health |
|---|---|---|---|---|
| Northwind | Team | 48 | $1,440 | 92 |
| Contoso | Enterprise | 210 | $8,900 | 61 |
| Fabrikam | Starter | 6 | $90 | 18 |
| Total | $10,430 | |||
<div class="_78-table-wrap">
<table class="_78-table">
<caption>Customers by monthly recurring revenue</caption>
<thead>
<tr>
<th>Customer</th><th>Plan</th><th class="_78-right">Seats</th>
<th class="_78-right">MRR</th><th class="_78-center">Health</th>
</tr>
</thead>
<tbody>
<tr>
<td class="_78-fw-600">Northwind</td>
<td><span class="_78-badge _78-badge-success">Team</span></td>
<td class="_78-right">48</td>
<td class="_78-right _78-text-success">$1,440</td>
<td class="_78-center"><span class="_78-score-pill _78-high">92</span></td>
</tr>
<tr class="_78-selected">
<td class="_78-fw-600">Contoso</td>
<td><span class="_78-badge _78-badge-accent">Enterprise</span></td>
<td class="_78-right">210</td>
<td class="_78-right _78-text-success">$8,900</td>
<td class="_78-center"><span class="_78-score-pill _78-mid">61</span></td>
</tr>
<tr>
<td class="_78-fw-600">Fabrikam</td>
<td><span class="_78-badge _78-badge-dim">Starter</span></td>
<td class="_78-right">6</td>
<td class="_78-right">$90</td>
<td class="_78-center"><span class="_78-score-pill _78-low">18</span></td>
</tr>
</tbody>
<tfoot>
<tr><td colspan="3">Total</td><td class="_78-right">$10,430</td><td></td></tr>
</tfoot>
</table>
</div>
Parts and helpers
| Class or element | What it does |
|---|---|
_78-table-wrap | Scroll container. Also insets the first column so text lines up with the surrounding card. |
<caption> | Styled as a small dim heading above the table. Give every table one — add _78-sr-only to keep it for screen readers only. |
<tfoot> | Totals row: a top border and heavier weight, no hover. |
_78-right | Right-aligns a cell and switches on tabular figures, so columns of numbers line up. |
_78-center | Center-aligns a cell. Works on th and td. |
_78-selected | On a <tr> — paints the row with --row-selected. |
_78-right brings font-variant-numeric: tabular-nums with it, so you do not
also need _78-nums on a right-aligned number column.
Variants
All three combine freely with each other and with the wrapper.
| Class | Effect |
|---|---|
_78-table-compact | Tighter cell padding, for dense lists. |
_78-table-striped | Even rows get --bg3; hover still wins. |
_78-table-sticky | Header sticks while the body scrolls. Needs a scroll container with a height. |
| Event | Actor | Time |
|---|---|---|
| Workspace created | jordan | 09:31:04 |
| Member invited | jordan | 09:31:07 |
| Plan upgraded | sam | 09:31:12 |
| Billing updated | sam | 10:02:55 |
<div class="_78-table-wrap">
<table class="_78-table _78-table-compact _78-table-striped">
<caption class="_78-sr-only">Recent activity</caption>
<thead><tr><th>Event</th><th>Actor</th><th class="_78-right">Time</th></tr></thead>
<tbody>
<tr><td>Workspace created</td><td class="_78-mono">jordan</td><td class="_78-right _78-nums">09:31:04</td></tr>
<tr><td>Member invited</td><td class="_78-mono">jordan</td><td class="_78-right _78-nums">09:31:07</td></tr>
<tr><td>Plan upgraded</td><td class="_78-mono">sam</td><td class="_78-right _78-nums">09:31:12</td></tr>
<tr><td>Billing updated</td><td class="_78-mono">sam</td><td class="_78-right _78-nums">10:02:55</td></tr>
</tbody>
</table>
</div>
Sticky header
The header pins itself to the top of the scroll container, so give the wrapper a
max-height. Without one there is nothing to scroll and nothing to stick to.
| Region | Accounts |
|---|---|
| North America | 1,204 |
| Europe | 986 |
| Asia Pacific | 655 |
| South America | 312 |
| Africa | 188 |
| Middle East | 141 |
<div class="_78-table-wrap" style="max-height:180px">
<table class="_78-table _78-table-compact _78-table-sticky">
<thead><tr><th>Region</th><th class="_78-right">Accounts</th></tr></thead>
<tbody>
<tr><td>North America</td><td class="_78-right">1,204</td></tr>
<tr><td>Europe</td><td class="_78-right">986</td></tr>
<tr><td>Asia Pacific</td><td class="_78-right">655</td></tr>
<tr><td>South America</td><td class="_78-right">312</td></tr>
<tr><td>Africa</td><td class="_78-right">188</td></tr>
<tr><td>Middle East</td><td class="_78-right">141</td></tr>
</tbody>
</table>
</div>
Table card and toolbar
Most data tables end up in the same frame: a card, and above the table a row of filters — a date
range, a checkbox or two, how many rows are showing, a way to clear it all.
_78-table-card and _78-table-filters are that frame, so the second table that
needs it reuses the classes rather than copying the first one's CSS. It works the same over a plain
_78-table or a Tabulator grid.
| When | Event | Rows |
|---|---|---|
| Oct 3 | Nightly import | 1,204 |
| Oct 3 | Error Webhook timed out | 0 |
| Oct 2 | Nightly import | 986 |
| Oct 2 | Error Rate limited | 0 |
<div class="_78-table-card">
<div class="_78-table-filters">
<div class="_78-field _78-table-dates">
<label for="log-from">From</label>
<input class="_78-input" id="log-from" type="date">
</div>
<div class="_78-field _78-table-dates">
<label for="log-to">To</label>
<input class="_78-input" id="log-to" type="date">
</div>
<label class="_78-check">
<input type="checkbox"> Errors only
<span class="_78-badge _78-badge-danger">2</span>
</label>
<span class="_78-grow"></span>
<span class="_78-table-count">Showing 4 of 4</span>
<button class="_78-btn _78-btn-ghost _78-btn-sm" type="button">Clear filters</button>
</div>
<div class="_78-table-wrap">
<table class="_78-table _78-table-compact">
<thead><tr><th>When</th><th>Event</th><th class="_78-right">Rows</th></tr></thead>
<tbody>
<tr><td>Oct 3</td><td>Nightly import</td><td class="_78-right">1,204</td></tr>
<tr><td>Oct 3</td><td><span class="_78-badge _78-badge-danger">Error</span> Webhook timed out</td><td class="_78-right">0</td></tr>
<tr><td>Oct 2</td><td>Nightly import</td><td class="_78-right">986</td></tr>
<tr><td>Oct 2</td><td><span class="_78-badge _78-badge-danger">Error</span> Rate limited</td><td class="_78-right">0</td></tr>
</tbody>
</table>
</div>
</div>
The toolbar wraps on a phone, and a _78-table-dates pair stays side by side. Labeled fields
line up by their bottoms; a checkbox, a count or a small button centers on the inputs' line. A
_78-grow spacer pushes the count and the clear button to the right.
Over a Tabulator
Put the Tabulator element where the _78-table-wrap was, and let Tabulator's own
setFilter do the filtering. Hide the clear button with hidden until a filter is
set:
<div class="_78-table-card">
<div class="_78-table-filters"> … the same toolbar … </div>
<div id="activity"></div>
</div>
<script>
var table = new Tabulator('#activity', { data: rows, layout: 'fitColumns', columns: [ … ] });
function apply() {
table.setFilter(function (r) {
return (!from.value || r.date >= from.value) && (!to.value || r.date <= to.value)
&& (!errorsOnly.checked || r.level === 'error');
});
}
table.on('dataFiltered', function (filters, shown) {
count.textContent = 'Showing ' + shown.length + ' of ' + rows.length;
});
</script>
| Class | What it does |
|---|---|
_78-table-card | The card: a card surface with tighter --space-3 padding. |
_78-table-filters | The toolbar: a wrapping row, bottoms aligned, --space-3 above the table. Fields lose their bottom margin. |
_78-table-dates | On a _78-field with a date input: 130–170px wide, so a From / To pair stays side by side on a phone. |
_78-table-count | The “Showing 12 of 40” readout: small, dim, tabular figures. |