Components
Tables
Real HTML tables for real tabular data. Header cells are scoped, numbers align to the inline end, and wide tables scroll inside their own container rather than breaking the page.
The default table
Wrap every table in .tmt-table-wrap. The wrapper draws the
border and the rounded corners, and — critically — gives the table somewhere to scroll
when it is wider than its column.
| Device | Status | Last seen | Power |
|---|---|---|---|
| Ceiling light | Online | 14:20 | 8.5 W |
| Air conditioner | Online | 14:19 | 1,240 W |
| Window sensor | Low battery | 13:02 | 0.1 W |
| Smart plug | Offline | Yesterday | — |
<div class="tmt-table-wrap">
<table class="tmt-table">
<caption>Devices in the living room</caption>
<thead>
<tr>
<th scope="col">Device</th>
<th scope="col">Status</th>
<th scope="col" class="tmt-numeric">Power</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Ceiling light</th>
<td>…</td>
<td class="tmt-numeric">8.5 W</td>
</tr>
</tbody>
</table>
</div>
Structure carries the meaning
A table that looks right and is marked up wrong is unusable with a screen reader, because the reader announces each cell with its headers. Four things do that work:
<caption>— names the table. It is the first thing announced, and it is visible, so it doubles as the table's title. Do not replace it with a heading above the table.scope="col"on every column header.scope="row"on the cell that identifies each row — and that cell should be a<th>, not a<td>. It is the row's name.<thead>and<tbody>— required for sticky headers to work and for the header styling to apply.
.tmt-grid or
.tmt-stack instead.
Alignment
- Text aligns to the inline start — left in English, right in Arabic.
The system uses
text-align: start, so this happens on its own. - Numbers align to the inline end so decimal points line up down the
column. Add
.tmt-numericto the header and every cell in the column; it also switches on tabular figures so the digits occupy equal width. - Cells align to the top. When one cell wraps to three lines, the rest of the row should not float in the middle.
- Dates and times are text, not numbers. Left-align them.
Variants
| Class | Effect | When |
|---|---|---|
.tmt-table--striped | Alternate rows get a sunken fill. | Wide tables where the eye loses the row. Not needed under about five columns. |
.tmt-table--hover | Highlights the row under the pointer. | Rows that are clickable or have row actions. |
.tmt-table--compact | Halves the vertical padding. | Internal tools where an operator scans many rows. |
.tmt-table--sticky | Pins the header while the body scrolls. | Long tables. Needs a fixed height on the wrapper. |
| Time | Room | kWh |
|---|---|---|
| 14:00 | Living room | 1.24 |
| 13:00 | Living room | 0.98 |
| 12:00 | Kitchen | 2.10 |
| 11:00 | Kitchen | 0.45 |
| 10:00 | Bedroom | 0.12 |
| 09:00 | Bedroom | 0.08 |
<div class="tmt-table-wrap" style="max-block-size: 12rem; overflow-y: auto;">
<table class="tmt-table tmt-table--striped tmt-table--compact
tmt-table--sticky tmt-table--hover">
…
</table>
</div>
Row actions
Put actions in the last column, as small ghost buttons. Each one needs an accessible name that identifies its row — "Edit" repeated down forty rows is useless to anyone navigating by a list of controls.
| Name | Runs | Actions |
|---|---|---|
| Morning lights | Weekdays, 06:30 |
|
| Away mode | Manual |
|
<td>
<button class="tmt-btn tmt-btn--ghost tmt-btn--sm">
Edit<span class="tmt-visually-hidden"> Morning lights</span>
</button>
</td>
The actions column header still needs a <th> — give it
a visually hidden label rather than leaving it empty, so the column is announced.
On small screens
The default is horizontal scrolling inside the wrapper, and that is usually the right answer: it keeps the data as a table, keeps the semantics intact, and keeps the relationship between columns visible.
Make the scroll discoverable and reachable:
<div class="tmt-table-wrap" tabindex="0" role="region"
aria-label="Devices in the living room">
<table class="tmt-table">…</table>
</div>
A scrollable region that is not focusable cannot be scrolled by keyboard at all, so add
tabindex="0" and a label whenever the table can overflow.
For a table with only two or three columns, stacking each row into a small card can read
better than scrolling. Do that with a separate mobile layout, not by changing the table's
display — overriding a table's display strips its semantics
from assistive technology.
Empty and loading
- Empty: keep the header row so the columns are still explained, and put one full-width cell underneath saying what would be here and how to add it.
- Loading: keep the table's height stable so the page does not jump when the rows arrive.
- A missing value is an em dash (—), not an empty cell, so it is clear the value is absent rather than the layout broken.