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.
An unhandled error has occurred.
Sedna.UI
v0.17.0 · main

Tables

tier 2 — classes

One class on the <table>. Write the thead and tbody 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 and the hover fill. 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 Wrap the table in a .sedna-scroll-x container, so the page body never scrolls sideways.

Extensions

Sortable, numeric and selected

Put a <button class="th-sort"> inside the header cell filling it, and set aria-sort — the arrow is keyed off the attribute a screen reader announces, so the two cannot disagree. .col-num goes on the th and the td; row selection is aria-selected on the <tr>. The header buttons here do nothing — which column is sorted is your query, and the arrow follows aria-sort.

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>

Sorting from C#

Live — sort it. SednaSort holds which column and which direction, and AriaSort(column) renders the attribute on every sortable header — none on the others, which is what shows the hover arrow. Toggle(column) starts a new column ascending and turns the sorted one; thenNone: true returns a descending column to unsorted. The ordering is still your query, and ToQuery() / FromQuery() carry the sort in an address as age or -age.

ORD-418772 min12
ORD-420422 min3
ORD-42094 min0
<table class="table">
    <thead>
        <tr>
            <th aria-sort="@_sort.AriaSort("order")">
                <button class="th-sort" type="button" @onclick="@(() => Sort("order"))">Order</button>
            </th>
            <th aria-sort="@_sort.AriaSort("age")">
                <button class="th-sort" type="button" @onclick="@(() => Sort("age"))">Age</button>
            </th>
            <th class="col-num" aria-sort="@_sort.AriaSort("reopens")">
                <button class="th-sort" type="button" @onclick="@(() => Sort("reopens"))">Reopens</button>
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var row in Rows())
        {
            <tr><td>@row.Order</td><td class="col-muted">@row.Age min</td><td class="col-num">@row.Reopens</td></tr>
        }
    </tbody>
</table>

@code {
    private sealed record Row(string Order, int Age, int Reopens);

    private static readonly Row[] All =
    [
        new("ORD-4204", 22, 3),
        new("ORD-4209", 4, 0),
        new("ORD-4187", 72, 12),
    ];

    // The sort is the app's state. SednaSort only answers aria-sort for each header,
    // which is what the arrow follows and what a screen reader announces.
    private SednaSort _sort = SednaSort.By("age", descending: true);

    private void Sort(string column) => _sort = _sort.Toggle(column);

    // The ordering itself stays the app's query — here a LINQ sort, in an app a database one.
    private IEnumerable<Row> Rows()
    {
        IEnumerable<Row> rows = _sort.Column switch
        {
            "order" => All.OrderBy(r => r.Order, StringComparer.Ordinal),
            "age" => All.OrderBy(r => r.Age),
            "reopens" => All.OrderBy(r => r.Reopens),
            _ => All,
        };
        return _sort.Descending ? rows.Reverse() : rows;
    }
}

.table--sticky needs a scroll container around the table with a max-height, and silently does nothing without one: the header sticks to the nearest scrolling ancestor, and with none that is the page, where the topbar already is.

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

Pinned column

.table--pin-start holds the first column still while the rest of a wide table scrolls under it — use it for the column a row is identified by; .table--pin-end does the same at the other edge, where a row's actions are. Both need a horizontal scroll container, .sedna-scroll-x, and neither combines with .table--stack, which has no sideways scroll to pin against. Scroll this table both ways.

Staff ID Surname First name E-mail Company Location Department Cost centre Manager Card
10020266AebischerNadinenadine.aebischer@example.comNorthwind RetailZürichField Service4210M. Brunner8841
10020412BaumgartnerTobiastobias.baumgartner@example.comNorthwind RetailBernApplication Management4180S. Kaufmann8912
10021077CavalliElenaelena.cavalli@example.comNorthwind ItalyMilanoField Service4210M. Brunner9034
10021340DörigSimonsimon.doerig@example.comNorthwind RetailSt. GallenInfrastructure4090P. Weber9101
10021688EichenbergerJudithjudith.eichenberger@example.comNorthwind RetailZürichService Desk4155S. Kaufmann9188
10022019FreiMarcmarc.frei@example.comNorthwind GermanyStuttgartInfrastructure4090P. Weber9240
<div class="sedna-scroll sedna-scroll-x" style="--scroll-max: 220px" tabindex="0" role="group" aria-label="Employee register">
    <table class="table table--sticky table--zebra table--pin-start">
        <thead>
            <tr>
                <th>Staff ID</th>
                <th>Surname</th>
                <th>First name</th>
                <th>E-mail</th>
                <th>Company</th>
                <th>Location</th>
                <th>Department</th>
                <th>Cost centre</th>
                <th>Manager</th>
                <th class="col-num">Card</th>
            </tr>
        </thead>
        <tbody>
            <tr><td>10020266</td><td>Aebischer</td><td>Nadine</td><td>nadine.aebischer@example.com</td><td>Northwind Retail</td><td>Zürich</td><td>Field Service</td><td>4210</td><td>M. Brunner</td><td class="col-num">8841</td></tr>
            <tr aria-selected="true"><td>10020412</td><td>Baumgartner</td><td>Tobias</td><td>tobias.baumgartner@example.com</td><td>Northwind Retail</td><td>Bern</td><td>Application Management</td><td>4180</td><td>S. Kaufmann</td><td class="col-num">8912</td></tr>
            <tr><td>10021077</td><td>Cavalli</td><td>Elena</td><td>elena.cavalli@example.com</td><td>Northwind Italy</td><td>Milano</td><td>Field Service</td><td>4210</td><td>M. Brunner</td><td class="col-num">9034</td></tr>
            <tr><td>10021340</td><td>Dörig</td><td>Simon</td><td>simon.doerig@example.com</td><td>Northwind Retail</td><td>St. Gallen</td><td>Infrastructure</td><td>4090</td><td>P. Weber</td><td class="col-num">9101</td></tr>
            <tr><td>10021688</td><td>Eichenberger</td><td>Judith</td><td>judith.eichenberger@example.com</td><td>Northwind Retail</td><td>Zürich</td><td>Service Desk</td><td>4155</td><td>S. Kaufmann</td><td class="col-num">9188</td></tr>
            <tr><td>10022019</td><td>Frei</td><td>Marc</td><td>marc.frei@example.com</td><td>Northwind Germany</td><td>Stuttgart</td><td>Infrastructure</td><td>4090</td><td>P. Weber</td><td class="col-num">9240</td></tr>
        </tbody>
    </table>
</div>

Pinned actions

.table--pin-end on its own, for the commonest reason to pin the far edge: the row's actions stay reachable however wide the table is. .col-fit on the header and the cell keeps the column to its buttons, and the edge shadow appears only while there is a column scrolled under it. Narrow this window to see it.

Reservation Order Customer From To Reason Confidence Actions
rsv-20419ORD-4209Priya NairEU-CentralEU-West Carrier capacity in Hamburg is exhausted until Thursday0.87
rsv-20420ORD-4213Tomás FerreiraEU-WestEU-Central Customer address changed after the pick list was printed0.64
rsv-20424ORD-4250Alex FischerEU-CentralEU-Central Split shipment: two lines are back-ordered until the next inbound0.91
<div class="sedna-scroll-x" tabindex="0" role="group" aria-label="Pending reservations">
    <table class="table table--pin-end">
        <thead>
            <tr>
                <th>Reservation</th>
                <th>Order</th>
                <th>Customer</th>
                <th>From</th>
                <th>To</th>
                <th>Reason</th>
                <th class="col-num">Confidence</th>
                <th class="col-fit"><span class="visually-hidden">Actions</span></th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>rsv-20419</td><td>ORD-4209</td><td>Priya Nair</td><td>EU-Central</td><td>EU-West</td>
                <td>Carrier capacity in Hamburg is exhausted until Thursday</td><td class="col-num">0.87</td>
                <td class="col-fit">
                    <div class="sedna-row sedna-gap-0">
                        <button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Approve rsv-20419"><i class="ri-check-line"></i></button>
                        <button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Decline rsv-20419"><i class="ri-close-line"></i></button>
                    </div>
                </td>
            </tr>
            <tr>
                <td>rsv-20420</td><td>ORD-4213</td><td>Tomás Ferreira</td><td>EU-West</td><td>EU-Central</td>
                <td>Customer address changed after the pick list was printed</td><td class="col-num">0.64</td>
                <td class="col-fit">
                    <div class="sedna-row sedna-gap-0">
                        <button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Approve rsv-20420"><i class="ri-check-line"></i></button>
                        <button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Decline rsv-20420"><i class="ri-close-line"></i></button>
                    </div>
                </td>
            </tr>
            <tr>
                <td>rsv-20424</td><td>ORD-4250</td><td>Alex Fischer</td><td>EU-Central</td><td>EU-Central</td>
                <td>Split shipment: two lines are back-ordered until the next inbound</td><td class="col-num">0.91</td>
                <td class="col-fit">
                    <div class="sedna-row sedna-gap-0">
                        <button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Approve rsv-20424"><i class="ri-check-line"></i></button>
                        <button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Decline rsv-20424"><i class="ri-close-line"></i></button>
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
</div>

Column widths

.col-control gives a cell holding an input, a select or a .value-trigger a floor and makes the control fill it; .col-prose lets a sentence or a distinguished name give way instead of taking the table; .col-fit takes the width of its content and no more. Put each on the th and the td, and set --col-control-min, --col-control-max, --col-prose-min or --col-prose-max on the cell where a column needs a different number.

AD attribute HR field Last result Actions
Mapped Written for 1 204 of 1 211 accounts.
Failed Declined by CN=svc-hr-sync,OU=Service Accounts,OU=Managed,DC=example,DC=com — insufficient access rights on the target container.
<div class="sedna-scroll-x">
    <table class="table">
        <thead>
            <tr>
                <th class="col-fit"></th>
                <th class="col-control">AD attribute</th>
                <th class="col-control">HR field</th>
                <th class="col-prose">Last result</th>
                <th class="col-fit">Actions</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td class="col-fit"><i class="ri-checkbox-circle-line"></i><span class="visually-hidden">Mapped</span></td>
                <td class="col-control">
                    <select class="form-select" aria-label="AD attribute for Location">
                        <option>physicalDeliveryOfficeName</option>
                        <option>givenName</option>
                        <option>department</option>
                    </select>
                </td>
                <td class="col-control">
                    <select class="form-select" aria-label="HR field for physicalDeliveryOfficeName">
                        <option>Location</option>
                        <option>FirstName</option>
                        <option>Department</option>
                    </select>
                </td>
                <td class="col-prose">Written for 1 204 of 1 211 accounts.</td>
                <td class="col-fit"><button class="btn btn-sm" type="button">Edit</button></td>
            </tr>
            <tr>
                <td class="col-fit"><i class="ri-error-warning-line"></i><span class="visually-hidden">Failed</span></td>
                <td class="col-control">
                    <select class="form-select" aria-label="AD attribute for CostCentre">
                        <option>extensionAttribute7</option>
                        <option>employeeNumber</option>
                    </select>
                </td>
                <td class="col-control">
                    <select class="form-select" aria-label="HR field for extensionAttribute7">
                        <option>CostCentre</option>
                        <option>StaffId</option>
                    </select>
                </td>
                <td class="col-prose">
                    Declined by CN=svc-hr-sync,OU=Service Accounts,OU=Managed,DC=example,DC=com —
                    insufficient access rights on the target container.
                </td>
                <td class="col-fit"><button class="btn btn-sm" type="button">Edit</button></td>
            </tr>
        </tbody>
    </table>
</div>

Expandable rows

The detail row is a real <tr class="tr-detail"> immediately after its parent, and its cell spans every column. Shown expanded — which row is open is your app's state, with aria-expanded on the control that toggles it.

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>

Grouped rows

A .tr-group row heads the rows after it — a state, a day, a customer. Its one cell is a <th scope="rowgroup"> spanning the table, so a screen reader announces the group with every row under it, and the .tr-group-body inside it is a flex row that takes whatever belongs to the group: a sentence, a badge for the count, a chip for the filter that made it, with .tr-group-count at the far end. Under .table--sticky the group row pins beneath the header.

OrderCustomerReservationValue
Waiting on you 3 EU-Central 3 of 128
ORD-4209Priya NairReroute to EU-West1,240.00
ORD-4211Tomás FerreiraHold for address check318.40
ORD-4250Alex FischerSplit shipment2,080.00
Decided today 2 2 of 128
ORD-4204Nadine AebischerAsk for the client version96.00
ORD-4198Simon DörigApprove as offered440.00
<table class="table">
    <thead>
        <tr><th>Order</th><th>Customer</th><th>Reservation</th><th class="col-num">Value</th></tr>
    </thead>
    <tbody>
        <tr class="tr-group">
            <th colspan="4" scope="rowgroup">
                <div class="tr-group-body">
                    Waiting on you
                    <span class="badge badge-warn">3</span>
                    <span class="chip">EU-Central <button class="chip-dismiss" type="button" aria-label="Remove the EU-Central filter"><i class="ri-close-line"></i></button></span>
                    <span class="tr-group-count">3 of 128</span>
                </div>
            </th>
        </tr>
        <tr><td>ORD-4209</td><td>Priya Nair</td><td>Reroute to EU-West</td><td class="col-num">1,240.00</td></tr>
        <tr><td>ORD-4211</td><td>Tomás Ferreira</td><td>Hold for address check</td><td class="col-num">318.40</td></tr>
        <tr><td>ORD-4250</td><td>Alex Fischer</td><td>Split shipment</td><td class="col-num">2,080.00</td></tr>
        <tr class="tr-group">
            <th colspan="4" scope="rowgroup">
                <div class="tr-group-body">
                    Decided today
                    <span class="badge badge-go">2</span>
                    <span class="tr-group-count">2 of 128</span>
                </div>
            </th>
        </tr>
        <tr><td>ORD-4204</td><td>Nadine Aebischer</td><td>Ask for the client version</td><td class="col-num">96.00</td></tr>
        <tr><td>ORD-4198</td><td>Simon Dörig</td><td>Approve as offered</td><td class="col-num">440.00</td></tr>
    </tbody>
</table>

A filter per column

.tr-filter is a second header row holding a .form-input-sm or a .form-select per column, for an admin table filtered column by column — the toolbar above a table is still the default. Give every control an aria-label: the heading above it is not its label. Under .table--sticky it pins beneath the header, and a .tr-group row pins beneath both; set --table-filter-height on the table if your controls are not the small size.

OrderStatusOwner
ORD-4209OpenAlex Fischer
ORD-4204HeldPriya Nair
ORD-4187OpenJordan Weiss
ORD-4182OpenAlex Fischer
ORD-4176HeldPriya Nair
ORD-4171OpenJordan Weiss
ORD-4166OpenAlex Fischer
<div class="sedna-scroll" style="max-height:260px">
    <table class="table table--sticky">
        <thead>
            <tr><th>Order</th><th>Status</th><th>Owner</th></tr>
            <tr class="tr-filter">
                <th><input class="form-input form-input-sm" type="search" placeholder="ORD-…" aria-label="Filter by order" /></th>
                <th>
                    <select class="form-input form-select form-input-sm" aria-label="Filter by status">
                        <option>Any</option><option>Open</option><option>Held</option>
                    </select>
                </th>
                <th><input class="form-input form-input-sm" type="search" aria-label="Filter by owner" /></th>
            </tr>
        </thead>
        <tbody>
            <tr><td>ORD-4209</td><td>Open</td><td>Alex Fischer</td></tr>
            <tr><td>ORD-4204</td><td>Held</td><td>Priya Nair</td></tr>
            <tr><td>ORD-4187</td><td>Open</td><td>Jordan Weiss</td></tr>
            <tr><td>ORD-4182</td><td>Open</td><td>Alex Fischer</td></tr>
            <tr><td>ORD-4176</td><td>Held</td><td>Priya Nair</td></tr>
            <tr><td>ORD-4171</td><td>Open</td><td>Jordan Weiss</td></tr>
            <tr><td>ORD-4166</td><td>Open</td><td>Alex Fischer</td></tr>
        </tbody>
    </table>
</div>

A filter for each type of column

Each filter is a control that already exists, at the small size, so the row stays one height. Text is an .input-group--sm with a search affix and an .input-clear, which shows only while the field has a value. One choice is a .form-select-sm, holding badges through <selectedcontent> where the values are states. Several choices, and a date range, are a .form-trigger: a button shaped like the field beside it that reads 2 selected and opens a popover of checkboxes or a date picker. A number range is two fields in one group, and yes / no is a .segmented--sm with Any. .col-filtered on the heading and the filter cell marks the columns that are set. Open the region and the date.

Host, filtered Environment Region, filtered Status CPU Last deploy, filtered Monitored
Region
September 2026
orders-console-01 Production EU-West Running 42% 11 Sep 2026, 16:40 Yes
orders-console-02 Production EU-Central Degraded 91% 10 Sep 2026, 09:15 Yes
orders-api-04 Staging EU-West Running 17% 9 Sep 2026, 14:02 No
orders-worker-07 Production EU-Central Stopped 0% 7 Sep 2026, 11:30 Yes
<div class="sedna-scroll-x">
    <table class="table">
        <thead>
            <tr>
                <th class="col-filtered">Host<span class="visually-hidden">, filtered</span></th>
                <th>Environment</th>
                <th class="col-filtered">Region<span class="visually-hidden">, filtered</span></th>
                <th>Status</th>
                <th class="col-num">CPU</th>
                <th class="col-filtered">Last deploy<span class="visually-hidden">, filtered</span></th>
                <th>Monitored</th>
            </tr>
            <tr class="tr-filter">
                <th class="col-control col-filtered" style="--col-control-min: 11rem">
                    <div class="input-group input-group--sm">
                        <span class="input-affix"><i class="ri-search-line"></i></span>
                        <input class="form-input" type="search" value="orders" placeholder="Host name" aria-label="Filter by host" />
                        <button class="input-clear" type="button" aria-label="Clear the host filter"><i class="ri-close-line"></i></button>
                    </div>
                </th>
                <th class="col-control" style="--col-control-min: 8rem">
                    <select class="form-select form-select-sm" aria-label="Filter by environment">
                        <option>Any</option>
                        <option>Production</option>
                        <option>Staging</option>
                    </select>
                </th>
                <th class="col-control col-filtered" style="--col-control-min: 9rem">
                    <button class="form-input form-input-sm form-trigger" type="button" popovertarget="ft-region" aria-label="Filter by region, 2 selected">
                        <span>2 selected</span>
                    </button>
                    <div class="popover" id="ft-region" popover>
                        <strong class="popover-title">Region</strong>
                        <div class="sedna-col">
                            <label class="form-check"><input type="checkbox" checked /> EU-West</label>
                            <label class="form-check"><input type="checkbox" checked /> EU-Central</label>
                            <label class="form-check"><input type="checkbox" /> US-East</label>
                            <label class="form-check"><input type="checkbox" /> AP-South</label>
                        </div>
                        <div class="form-actions form-actions--split">
                            <button class="btn btn-sm btn-ghost" type="button">Clear</button>
                            <button class="btn btn-sm btn-primary" type="button" popovertarget="ft-region" popovertargetaction="hide">Apply</button>
                        </div>
                    </div>
                </th>
                <th class="col-control" style="--col-control-min: 9rem">
                    <select class="form-select form-select-sm" aria-label="Filter by status">
                        <button>
                            <selectedcontent></selectedcontent>
                        </button>
                        <option value="" selected>Any</option>
                        <option value="running"><span class="badge badge-go">Running</span></option>
                        <option value="degraded"><span class="badge badge-warn">Degraded</span></option>
                        <option value="stopped"><span class="badge badge-danger">Stopped</span></option>
                    </select>
                </th>
                <th class="col-control col-num" style="--col-control-min: 8rem">
                    <div class="input-group input-group--sm">
                        <input class="form-input" type="number" placeholder="Min" aria-label="Lowest CPU, in percent" />
                        <span class="input-affix">–</span>
                        <input class="form-input" type="number" placeholder="Max" aria-label="Highest CPU, in percent" />
                    </div>
                </th>
                <th class="col-control col-filtered" style="--col-control-min: 10rem">
                    <button class="form-input form-input-sm form-trigger" type="button" popovertarget="ft-deployed" aria-label="Filter by last deploy, 7 to 11 September 2026">
                        <i class="ri-calendar-line"></i><span>7–11 Sep 2026</span>
                    </button>
                    <div class="popover datepicker" id="ft-deployed" popover>
                        <div class="datepicker-head">
                            <button class="btn btn-ghost btn-icon btn-sm" type="button" aria-label="Previous month"><i class="ri-arrow-left-s-line"></i></button>
                            <span class="datepicker-title">September 2026</span>
                            <button class="btn btn-ghost btn-icon btn-sm" type="button" aria-label="Next month"><i class="ri-arrow-right-s-line"></i></button>
                        </div>
                        <div class="cal-month cal-month--mini">
                            <div class="cal-weekdays" aria-hidden="true">
                                <span class="cal-weekday">Mo</span>
                                <span class="cal-weekday">Tu</span>
                                <span class="cal-weekday">We</span>
                                <span class="cal-weekday">Th</span>
                                <span class="cal-weekday">Fr</span>
                                <span class="cal-weekday">Sa</span>
                                <span class="cal-weekday">Su</span>
                            </div>
                            <div class="cal-row">
                                <span class="cal-day cal-day--outside"></span>
                                <button class="cal-day" type="button" aria-label="Tuesday 1 September 2026" aria-pressed="false">1</button>
                                <button class="cal-day" type="button" aria-label="Wednesday 2 September 2026" aria-pressed="false">2</button>
                                <button class="cal-day" type="button" aria-label="Thursday 3 September 2026" aria-pressed="false">3</button>
                                <button class="cal-day" type="button" aria-label="Friday 4 September 2026" aria-pressed="false">4</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Saturday 5 September 2026" aria-pressed="false">5</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Sunday 6 September 2026" aria-pressed="false">6</button>
                            </div>
                            <div class="cal-row">
                                <button class="cal-day cal-day--range-start" type="button" aria-label="Monday 7 September 2026" aria-pressed="true">7</button>
                                <button class="cal-day cal-day--in-range" type="button" aria-label="Tuesday 8 September 2026" aria-pressed="true">8</button>
                                <button class="cal-day cal-day--in-range" type="button" aria-label="Wednesday 9 September 2026" aria-pressed="true">9</button>
                                <button class="cal-day cal-day--in-range" type="button" aria-label="Thursday 10 September 2026" aria-pressed="true">10</button>
                                <button class="cal-day cal-day--range-end" type="button" aria-label="Friday 11 September 2026" aria-pressed="true">11</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Saturday 12 September 2026" aria-pressed="false">12</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Sunday 13 September 2026" aria-pressed="false">13</button>
                            </div>
                            <div class="cal-row">
                                <button class="cal-day" type="button" aria-label="Monday 14 September 2026" aria-pressed="false">14</button>
                                <button class="cal-day" type="button" aria-label="Tuesday 15 September 2026" aria-pressed="false" aria-current="date">15</button>
                                <button class="cal-day" type="button" aria-label="Wednesday 16 September 2026" aria-pressed="false" disabled>16</button>
                                <button class="cal-day" type="button" aria-label="Thursday 17 September 2026" aria-pressed="false" disabled>17</button>
                                <button class="cal-day" type="button" aria-label="Friday 18 September 2026" aria-pressed="false" disabled>18</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Saturday 19 September 2026" aria-pressed="false" disabled>19</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Sunday 20 September 2026" aria-pressed="false" disabled>20</button>
                            </div>
                            <div class="cal-row">
                                <button class="cal-day" type="button" aria-label="Monday 21 September 2026" aria-pressed="false" disabled>21</button>
                                <button class="cal-day" type="button" aria-label="Tuesday 22 September 2026" aria-pressed="false" disabled>22</button>
                                <button class="cal-day" type="button" aria-label="Wednesday 23 September 2026" aria-pressed="false" disabled>23</button>
                                <button class="cal-day" type="button" aria-label="Thursday 24 September 2026" aria-pressed="false" disabled>24</button>
                                <button class="cal-day" type="button" aria-label="Friday 25 September 2026" aria-pressed="false" disabled>25</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Saturday 26 September 2026" aria-pressed="false" disabled>26</button>
                                <button class="cal-day cal-day--weekend" type="button" aria-label="Sunday 27 September 2026" aria-pressed="false" disabled>27</button>
                            </div>
                            <div class="cal-row">
                                <button class="cal-day" type="button" aria-label="Monday 28 September 2026" aria-pressed="false" disabled>28</button>
                                <button class="cal-day" type="button" aria-label="Tuesday 29 September 2026" aria-pressed="false" disabled>29</button>
                                <button class="cal-day" type="button" aria-label="Wednesday 30 September 2026" aria-pressed="false" disabled>30</button>
                                <span class="cal-day cal-day--outside"></span>
                                <span class="cal-day cal-day--outside"></span>
                                <span class="cal-day cal-day--outside"></span>
                                <span class="cal-day cal-day--outside"></span>
                            </div>
                        </div>
                        <div class="datepicker-actions">
                            <button class="btn btn-sm btn-ghost" type="button">Any time</button>
                            <button class="btn btn-sm" type="button" popovertarget="ft-deployed" popovertargetaction="hide">Cancel</button>
                            <button class="btn btn-sm btn-primary" type="button" popovertarget="ft-deployed" popovertargetaction="hide">Apply</button>
                        </div>
                    </div>
                </th>
                <th class="col-control" style="--col-control-min: 9rem">
                    <div class="segmented segmented--sm" role="group" aria-label="Filter by monitoring">
                        <label class="segmented-option"><input type="radio" name="ft-monitored" checked />Any</label>
                        <label class="segmented-option"><input type="radio" name="ft-monitored" />Yes</label>
                        <label class="segmented-option"><input type="radio" name="ft-monitored" />No</label>
                    </div>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>orders-console-01</td>
                <td>Production</td>
                <td>EU-West</td>
                <td><span class="badge badge-go">Running</span></td>
                <td class="col-num">42%</td>
                <td class="col-muted">11 Sep 2026, 16:40</td>
                <td>Yes</td>
            </tr>
            <tr>
                <td>orders-console-02</td>
                <td>Production</td>
                <td>EU-Central</td>
                <td><span class="badge badge-warn">Degraded</span></td>
                <td class="col-num">91%</td>
                <td class="col-muted">10 Sep 2026, 09:15</td>
                <td>Yes</td>
            </tr>
            <tr>
                <td>orders-api-04</td>
                <td>Staging</td>
                <td>EU-West</td>
                <td><span class="badge badge-go">Running</span></td>
                <td class="col-num">17%</td>
                <td class="col-muted">9 Sep 2026, 14:02</td>
                <td>No</td>
            </tr>
            <tr>
                <td>orders-worker-07</td>
                <td>Production</td>
                <td>EU-Central</td>
                <td><span class="badge badge-danger">Stopped</span></td>
                <td class="col-num">0%</td>
                <td class="col-muted">7 Sep 2026, 11:30</td>
                <td>Yes</td>
            </tr>
        </tbody>
    </table>
</div>

Several filters at once

With more than one filter set, repeat them above the table as .chip--active chips, each removable on its own, and end the row with Reset all. The chips are what a reader sees without scanning the filter row, and the count says how much the filters took away. Every filtered heading says so to a screen reader too, in a .visually-hidden span — the colour and the rule under it are not a label.

Status: Open, Held Amount: 100–500 Region: EU-West
6 of 482 orders
Order Customer Status, filtered Amount, filtered Region, filtered
Status
ORD-4209Alex FischerOpen318.50EU-West
ORD-4204Priya NairHeld142.00EU-West
ORD-4187Jordan WeissOpen489.90EU-West
ORD-4182Alex FischerOpen104.25EU-West
ORD-4176Priya NairHeld276.00EU-West
ORD-4171Jordan WeissOpen199.99EU-West
<div class="toolbar">
    <div class="toolbar-filters">
        <span class="chip chip--active">
            <span class="chip-label">Status: Open, Held</span>
            <button class="chip-dismiss" type="button" aria-label="Remove the status filter"><i class="ri-close-line"></i></button>
        </span>
        <span class="chip chip--active">
            <span class="chip-label">Amount: 100–500</span>
            <button class="chip-dismiss" type="button" aria-label="Remove the amount filter"><i class="ri-close-line"></i></button>
        </span>
        <span class="chip chip--active">
            <span class="chip-label">Region: EU-West</span>
            <button class="chip-dismiss" type="button" aria-label="Remove the region filter"><i class="ri-close-line"></i></button>
        </span>
        <button class="btn btn-sm btn-ghost" type="button"><i class="ri-filter-off-line"></i> Reset all</button>
    </div>
    <div class="toolbar-actions">
        <span class="toolbar-count">6 of 482 orders</span>
    </div>
</div>
<div class="sedna-scroll" style="max-height:280px">
    <table class="table table--sticky">
        <thead>
            <tr>
                <th>Order</th>
                <th>Customer</th>
                <th class="col-filtered">Status<span class="visually-hidden">, filtered</span></th>
                <th class="col-num col-filtered">Amount<span class="visually-hidden">, filtered</span></th>
                <th class="col-filtered">Region<span class="visually-hidden">, filtered</span></th>
            </tr>
            <tr class="tr-filter">
                <th class="col-control" style="--col-control-min: 8rem">
                    <div class="input-group input-group--sm">
                        <span class="input-affix"><i class="ri-search-line"></i></span>
                        <input class="form-input" type="search" placeholder="ORD-…" aria-label="Filter by order" />
                        <button class="input-clear" type="button" aria-label="Clear the order filter"><i class="ri-close-line"></i></button>
                    </div>
                </th>
                <th class="col-control" style="--col-control-min: 9rem">
                    <div class="input-group input-group--sm">
                        <span class="input-affix"><i class="ri-search-line"></i></span>
                        <input class="form-input" type="search" placeholder="Name" aria-label="Filter by customer" />
                        <button class="input-clear" type="button" aria-label="Clear the customer filter"><i class="ri-close-line"></i></button>
                    </div>
                </th>
                <th class="col-control col-filtered" style="--col-control-min: 8rem">
                    <button class="form-input form-input-sm form-trigger" type="button" popovertarget="fa-status" aria-label="Filter by status, 2 selected">
                        <span>2 selected</span>
                    </button>
                    <div class="popover" id="fa-status" popover>
                        <strong class="popover-title">Status</strong>
                        <div class="sedna-col">
                            <label class="form-check"><input type="checkbox" checked /> Open</label>
                            <label class="form-check"><input type="checkbox" checked /> Held</label>
                            <label class="form-check"><input type="checkbox" /> Shipped</label>
                            <label class="form-check"><input type="checkbox" /> Cancelled</label>
                        </div>
                        <div class="form-actions form-actions--split">
                            <button class="btn btn-sm btn-ghost" type="button">Clear</button>
                            <button class="btn btn-sm btn-primary" type="button" popovertarget="fa-status" popovertargetaction="hide">Apply</button>
                        </div>
                    </div>
                </th>
                <th class="col-control col-num col-filtered" style="--col-control-min: 8rem">
                    <div class="input-group input-group--sm">
                        <input class="form-input" type="number" value="100" placeholder="Min" aria-label="Lowest amount" />
                        <span class="input-affix">–</span>
                        <input class="form-input" type="number" value="500" placeholder="Max" aria-label="Highest amount" />
                    </div>
                </th>
                <th class="col-control col-filtered" style="--col-control-min: 8rem">
                    <select class="form-select form-select-sm" aria-label="Filter by region">
                        <option>Any</option>
                        <option selected>EU-West</option>
                        <option>EU-Central</option>
                        <option>US-East</option>
                    </select>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr><td>ORD-4209</td><td>Alex Fischer</td><td><span class="badge badge-info">Open</span></td><td class="col-num">318.50</td><td>EU-West</td></tr>
            <tr><td>ORD-4204</td><td>Priya Nair</td><td><span class="badge badge-warn">Held</span></td><td class="col-num">142.00</td><td>EU-West</td></tr>
            <tr><td>ORD-4187</td><td>Jordan Weiss</td><td><span class="badge badge-info">Open</span></td><td class="col-num">489.90</td><td>EU-West</td></tr>
            <tr><td>ORD-4182</td><td>Alex Fischer</td><td><span class="badge badge-info">Open</span></td><td class="col-num">104.25</td><td>EU-West</td></tr>
            <tr><td>ORD-4176</td><td>Priya Nair</td><td><span class="badge badge-warn">Held</span></td><td class="col-num">276.00</td><td>EU-West</td></tr>
            <tr><td>ORD-4171</td><td>Jordan Weiss</td><td><span class="badge badge-info">Open</span></td><td class="col-num">199.99</td><td>EU-West</td></tr>
        </tbody>
    </table>
</div>

A filter in the header

For a table too narrow for a filter row, a .th-filter button at the end of the heading opens the column's filter in a popover. .th-filter--active fills it and adds a dot while the filter is set; say the same in its aria-label. Beside a .th-sort, wrap the two in a .sedna-row.sedna-gap-0. Open both.

Order
Status, filtered
Status
Placed
ORD-4187Open14 Sep 2026
ORD-4209Open12 Sep 2026
ORD-4176Held9 Sep 2026
ORD-4204Held8 Sep 2026
<div class="card" style="max-width:360px">
    <table class="table">
        <thead>
            <tr>
                <th>Order</th>
                <th class="col-filtered">
                    <div class="sedna-row sedna-gap-0">
                        Status<span class="visually-hidden">, filtered</span>
                        <button class="th-filter th-filter--active" type="button" popovertarget="fp-status" aria-label="Filter by status, 2 chosen"><i class="ri-filter-3-line"></i></button>
                        <div class="popover" id="fp-status" popover>
                            <strong class="popover-title">Status</strong>
                            <div class="sedna-col">
                                <label class="form-check"><input type="checkbox" checked /> <span class="badge badge-info">Open</span></label>
                                <label class="form-check"><input type="checkbox" checked /> <span class="badge badge-warn">Held</span></label>
                                <label class="form-check"><input type="checkbox" /> <span class="badge badge-go">Shipped</span></label>
                                <label class="form-check"><input type="checkbox" /> <span class="badge badge-danger">Cancelled</span></label>
                            </div>
                            <div class="form-actions form-actions--split">
                                <button class="btn btn-sm btn-ghost" type="button">Clear</button>
                                <button class="btn btn-sm btn-primary" type="button" popovertarget="fp-status" popovertargetaction="hide">Apply</button>
                            </div>
                        </div>
                    </div>
                </th>
                <th aria-sort="descending">
                    <div class="sedna-row sedna-gap-0">
                        <button class="th-sort" type="button">Placed</button>
                        <button class="th-filter" type="button" popovertarget="fp-placed" aria-label="Filter by date placed"><i class="ri-filter-3-line"></i></button>
                        <div class="popover" id="fp-placed" popover>
                            <strong class="popover-title">Placed</strong>
                            <div class="sedna-col">
                                <div class="segmented segmented--sm" role="group" aria-label="Comparison">
                                    <label class="segmented-option"><input type="radio" name="fp-placed-op" />Before</label>
                                    <label class="segmented-option"><input type="radio" name="fp-placed-op" checked />On</label>
                                    <label class="segmented-option"><input type="radio" name="fp-placed-op" />After</label>
                                </div>
                                <input class="form-input form-input-sm" type="date" aria-label="Date placed" />
                            </div>
                            <div class="form-actions form-actions--split">
                                <button class="btn btn-sm btn-ghost" type="button">Clear</button>
                                <button class="btn btn-sm btn-primary" type="button" popovertarget="fp-placed" popovertargetaction="hide">Apply</button>
                            </div>
                        </div>
                    </div>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr><td>ORD-4187</td><td><span class="badge badge-info">Open</span></td><td class="col-muted">14 Sep 2026</td></tr>
            <tr><td>ORD-4209</td><td><span class="badge badge-info">Open</span></td><td class="col-muted">12 Sep 2026</td></tr>
            <tr><td>ORD-4176</td><td><span class="badge badge-warn">Held</span></td><td class="col-muted">9 Sep 2026</td></tr>
            <tr><td>ORD-4204</td><td><span class="badge badge-warn">Held</span></td><td class="col-muted">8 Sep 2026</td></tr>
        </tbody>
    </table>
</div>

Nothing found, and refreshing

.tr-empty is the row for a query that returned nothing, inside a table that is still there with its toolbar and count; .tr-busy is the same row for a refresh in flight. Write colspan="999", never the real count: a browser clamps it to the columns the table has, so it spans them all and does not go stale when a column is added. A .state-art drawing sits centred above the text; the first load is skeleton rows instead.

OrderStatusOwnerLines
No order matches these filters. Clear one to see more.
OrderStatusOwnerLines
Refreshing orders…
<div class="sedna-col sedna-gap-3">
    <table class="table">
        <thead>
            <tr><th>Order</th><th>Status</th><th>Owner</th><th class="col-num">Lines</th></tr>
        </thead>
        <tbody>
            <tr class="tr-empty">
                <td colspan="999">
                    <svg class="state-art state-art--sm" aria-hidden="true"><use href="#filtered-out" /></svg>
                    No order matches these filters. Clear one to see more.
                </td>
            </tr>
        </tbody>
    </table>

    <table class="table">
        <thead>
            <tr><th>Order</th><th>Status</th><th>Owner</th><th class="col-num">Lines</th></tr>
        </thead>
        <tbody aria-busy="true">
            <tr class="tr-busy">
                <td colspan="999"><span class="spinner" aria-hidden="true"></span>Refreshing orders…</td>
            </tr>
        </tbody>
    </table>
</div>

Stacked on a narrow screen

.table--stack turns each row into a card below 640px, taking each cell's label from data-label; a cell without one — an actions cell — keeps the full width. It throws away column alignment, so it is right for records read one at a time and wrong for anything scanned down a column. Narrow this window to see it.

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>