Connection lost. Reconnecting… attempt 1 of 8
Paused. Your work is held on the server.
Could not reconnect.
This session has expired on the server.
DR.Simple_UI
Showing main. This can be ahead of the version your app has installed — check what a class says it needs before copying it. Releases (opens in a new tab)

Tables

tier 2 — classes

One class on the <table>. Write the thead and tbody in the page.

There is no <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.

OrderShort descriptionPriorityAge
ORD-4209Outlook will not start after updateP34 min
ORD-4204VPN disconnects every few minutesP122 min
ORD-4198Request: second monitorP41 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.

OrderPendingOwner
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.

3 of 128
OrderStateAge
ORD-4209Pending4 min
ORD-4204Approved22 min
ORD-4198Rejected1 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>
Wide tables: put the table in a container with 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-420422 min3
ORD-42094 min0
ORD-41871 h 12 min12
3 orders15
<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>

.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.

OrderAssignment groupAge (min)
ORD-4209EU-Central4
ORD-4204EU-West22
ORD-4198EU-Central37
ORD-4187SD-Applications72
ORD-4166EU-West118
ORD-4140EU-Central203
<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.

OrderReservation
ORD-4209Reroute to EU-West
Confidence0.87
Matched on"VPN" + "disconnect"
ORD-4204Ask 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.

OrderGroupPriority
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>