Tables
tier 2 — classes
One class on the <table>. Write the thead and tbody
in the page.
<DataTable> component and there will not be one. Tables are written
as markup in the page.
Basic
Headers are uppercased and muted by the class. .col-muted is for secondary
columns — timestamps, counts — and stops them from wrapping.
| Order | Short description | Priority | Age |
|---|---|---|---|
| ORD-4209 | Outlook will not start after update | P3 | 4 min |
| ORD-4204 | VPN disconnects every few minutes | P1 | 22 min |
| ORD-4198 | Request: second monitor | P4 | 1 h |
<table class="table">
<thead>
<tr><th>Order</th><th>Short description</th><th>Priority</th><th>Age</th></tr>
</thead>
<tbody>
<tr><td>ORD-4209</td><td>Outlook will not start after update</td><td><span class="badge badge-warn">P3</span></td><td class="col-muted">4 min</td></tr>
<tr><td>ORD-4204</td><td>VPN disconnects every few minutes</td><td><span class="badge badge-danger">P1</span></td><td class="col-muted">22 min</td></tr>
<tr><td>ORD-4198</td><td>Request: second monitor</td><td><span class="badge">P4</span></td><td class="col-muted">1 h</td></tr>
</tbody>
</table>
Clickable rows
.tr-clickable adds the pointer, the hover fill and
user-select:none so a double click does not select the text. Keep a real
focusable control in the row for keyboard users — a whole row cannot be tabbed to.
| Order | Pending | Owner | |
|---|---|---|---|
| Mailbox quota exceeded | 2 | unclaimed | |
| Printer offline in HQ-3 | 0 | J. Weber |
<table class="table">
<thead>
<tr><th>Order</th><th>Pending</th><th>Owner</th><th></th></tr>
</thead>
<tbody>
<tr class="tr-clickable">
<td>Mailbox quota exceeded</td>
<td><span class="badge badge-warn">2</span></td>
<td class="col-muted">unclaimed</td>
<td><button class="btn btn-sm btn-icon" aria-label="Open order"><i class="ri-arrow-right-line"></i></button></td>
</tr>
<tr class="tr-clickable">
<td>Printer offline in HQ-3</td>
<td><span class="badge">0</span></td>
<td class="col-muted">J. Weber</td>
<td><button class="btn btn-sm btn-icon" aria-label="Open order"><i class="ri-arrow-right-line"></i></button></td>
</tr>
</tbody>
</table>
With a toolbar, in a card
The standard list page: filter bar above, table inside a card.
| Order | State | Age |
|---|---|---|
| ORD-4209 | Pending | 4 min |
| ORD-4204 | Approved | 22 min |
| ORD-4198 | Rejected | 1 h |
<div class="toolbar">
<div class="toolbar-filters">
<input class="form-input toolbar-input" type="search" placeholder="Search…" />
<label class="form-check"><input type="checkbox" checked /> Only mine</label>
</div>
<span class="toolbar-count">3 of 128</span>
</div>
<div class="card">
<table class="table">
<thead>
<tr><th>Order</th><th>State</th><th>Age</th></tr>
</thead>
<tbody>
<tr><td>ORD-4209</td><td><span class="badge badge-warn">Pending</span></td><td class="col-muted">4 min</td></tr>
<tr><td>ORD-4204</td><td><span class="badge badge-go">Approved</span></td><td class="col-muted">22 min</td></tr>
<tr><td>ORD-4198</td><td><span class="badge badge-danger">Rejected</span></td><td class="col-muted">1 h</td></tr>
</tbody>
</table>
</div>
overflow-x:auto and add .dr-scroll to it, so the horizontal
scrollbar is themed and the page body never scrolls sideways.
Extensions
Each of these is opt-in, because each costs something. A sticky header needs a scroll container, zebra stripes fight with row selection, and the stacked layout throws away column alignment.
Sortable, numeric and selected
The sort arrow is keyed off aria-sort, which is what a screen reader
announces, so the arrow and the announcement cannot disagree. Put a
<button class="th-sort"> inside the header cell filling it — a target
that is only the label text is smaller than it looks. The arrow reserves its 8px even when
unsorted, so the header does not shift as the reader sorts one column after another.
.col-num goes on the th and the td: right
aligned and tabular, so digits line up by place value and two numbers can be compared by
shape. Row selection is aria-selected on the <tr>, and the
leading rule is what survives a colour-blind palette and a greyscale print.
The header buttons here do nothing, and that is the whole design. The
library draws the affordance and announces the state; which column is sorted and in
what order is your query, and reordering rows is what a Blazor page does natively. Set
aria-sort from that state and the arrow follows.
| ORD-4204 | 22 min | 3 |
| ORD-4209 | 4 min | 0 |
| ORD-4187 | 1 h 12 min | 12 |
| 3 orders | 15 |
<table class="table">
<thead>
<tr>
<th aria-sort="none"><button class="th-sort" type="button">Order</button></th>
<th aria-sort="descending"><button class="th-sort" type="button">Age</button></th>
<th class="col-num" aria-sort="none"><button class="th-sort" type="button">Reopens</button></th>
</tr>
</thead>
<tbody>
<tr aria-selected="true"><td>ORD-4204</td><td class="col-muted">22 min</td><td class="col-num">3</td></tr>
<tr><td>ORD-4209</td><td class="col-muted">4 min</td><td class="col-num">0</td></tr>
<tr><td>ORD-4187</td><td class="col-muted">1 h 12 min</td><td class="col-num">12</td></tr>
</tbody>
<tfoot>
<tr><td>3 orders</td><td></td><td class="col-num">15</td></tr>
</tfoot>
</table>
Sticky header
.table--sticky needs two things from the markup and silently does nothing
without them: a scroll container around the table with a max-height, because
the header sticks to the nearest scrolling ancestor and with none it sticks to the page
where the topbar already is; and border-collapse: separate, which the class
sets, because a collapsed table's borders belong to the table rather than the cell and its
header border would scroll away.
| Order | Assignment group | Age (min) |
|---|---|---|
| ORD-4209 | EU-Central | 4 |
| ORD-4204 | EU-West | 22 |
| ORD-4198 | EU-Central | 37 |
| ORD-4187 | SD-Applications | 72 |
| ORD-4166 | EU-West | 118 |
| ORD-4140 | EU-Central | 203 |
<div class="dr-scroll" style="max-height:180px; overflow-y:auto" tabindex="0" role="group" aria-label="Order queue">
<table class="table table--sticky table--zebra">
<thead><tr><th>Order</th><th>Assignment group</th><th class="col-num">Age (min)</th></tr></thead>
<tbody>
<tr><td>ORD-4209</td><td>EU-Central</td><td class="col-num">4</td></tr>
<tr><td>ORD-4204</td><td>EU-West</td><td class="col-num">22</td></tr>
<tr><td>ORD-4198</td><td>EU-Central</td><td class="col-num">37</td></tr>
<tr><td>ORD-4187</td><td>SD-Applications</td><td class="col-num">72</td></tr>
<tr><td>ORD-4166</td><td>EU-West</td><td class="col-num">118</td></tr>
<tr><td>ORD-4140</td><td>EU-Central</td><td class="col-num">203</td></tr>
</tbody>
</table>
</div>
Expandable rows
The detail row is a real <tr class="tr-detail"> after its parent, so it
moves with it when the table is sorted and is read after it. Its cell spans every column.
The inset rule on the left is the only thing saying it belongs to the row above — a plain
indent reads as an unrelated row that happens to start further in.
Shown expanded, and there is nothing here to collapse it. Which row is open
is your app's state — a bool per row, and aria-expanded on the
control that toggles it. There is no drSimpleUi.table: rendering rows is the
one thing a Blazor app already does better than a script could.
| Order | Reservation |
|---|---|
| ORD-4209 | Reroute to EU-West |
Confidence0.87
Matched on"VPN" + "disconnect" | |
| ORD-4204 | Ask for the client version |
<table class="table">
<thead><tr><th>Order</th><th>Reservation</th></tr></thead>
<tbody>
<tr class="tr-clickable">
<td>ORD-4209</td><td>Reroute to EU-West</td>
</tr>
<tr class="tr-detail">
<td colspan="2">
<div class="tr-detail-body">
<div class="kv"><span class="k">Confidence</span><span class="v">0.87</span></div>
<div class="kv"><span class="k">Matched on</span><span class="v">"VPN" + "disconnect"</span></div>
</div>
</td>
</tr>
<tr class="tr-clickable">
<td>ORD-4204</td><td>Ask for the client version</td>
</tr>
</tbody>
</table>
Stacked on a narrow screen
.table--stack turns each row into a card below 640px, taking each cell's label
from data-label. Understand the trade first: it throws away
column alignment, so comparing one value across rows stops being possible. It is right for
a list of records read one at a time, and wrong for anything scanned down a column.
Narrow this window to see it. A cell with no data-label — an actions cell —
keeps the full width instead of leaving an empty gutter.
| Order | Group | Priority | |
|---|---|---|---|
| ORD-4209 | EU-Central | P3 | |
| ORD-4204 | EU-West | P1 |
<table class="table table--stack">
<thead><tr><th>Order</th><th>Group</th><th>Priority</th><th></th></tr></thead>
<tbody>
<tr>
<td data-label="Order">ORD-4209</td>
<td data-label="Group">EU-Central</td>
<td data-label="Priority"><span class="badge badge-warn">P3</span></td>
<td><button class="btn btn-sm" type="button">Open</button></td>
</tr>
<tr>
<td data-label="Order">ORD-4204</td>
<td data-label="Group">EU-West</td>
<td data-label="Priority"><span class="badge badge-danger">P1</span></td>
<td><button class="btn btn-sm" type="button">Open</button></td>
</tr>
</tbody>
</table>