Skip to content

Table

wu-table styles a data <table>. Wide tables go in a wu-table-scroll region, which shows shadows on the sides that have more content. The select-all box and expandable rows need the kit script; everything else is CSS.

Class, attribute or variable Effect
wu-table Root. Full width, row lines, muted header
wu-table-num Part, on cells. End-aligned tabular figures
wu-table-sort Part, a <button> in a header cell. Sort arrows drawn in CSS
aria-sort on a header cell ascending or descending shows one arrow
wu-table-select Part, on the cells of a checkbox column. A checked box selects its row; the script keeps the header box in step
Selected row A checked wu-table-select box, or aria-selected="true" on a row of a role="grid" table. Tint and a bar at the start of the first cell; wins over striped and hover
wu-table-expand Part, a <button> with aria-expanded. The script shows or hides the detail row
wu-table-detail Part, a <tr hidden> right after its row, with one cell spanning all columns
wu-table-truncate Part, on a cell whose text sits in a child element. One line, ellipsis at the inline end
--wu-table--TruncateWidth Widest a truncated text gets, 16rem by default
<tfoot> Summary row: semibold, with a strong line above it
sm Less cell padding
striped Tint on even rows
hover Tint on the row under the pointer
bordered Outer border, rounded corners, lines between cells
sticky-header Header cells stick to the top, with a shadow once stuck
sticky-start First column sticks to the inline start of the scroll region
sticky-end Last column sticks to the inline end of the scroll region
animated The expand chevron turns, opt-in
wu-table-scroll Scroll region with scroll shadows
--wu-table--MaxHeight Height of the scroll region, so the header sticks inside it
--wu-table--Background Background of the scroll region and the sticky header
Invoices this quarter
InvoiceCustomerAmount
INV-1042Northwind$1,200.00
INV-1043Contoso$86.50
INV-1044Fabrikam$12,940.00
<table class="wu-table">
<caption>Invoices this quarter</caption>
<thead>
<tr><th scope="col">Invoice</th><th scope="col">Customer</th><th scope="col" class="wu-table-num">Amount</th></tr>
</thead>
<tbody>
<tr><th scope="row">INV-1042</th><td>Northwind</td><td class="wu-table-num">$1,200.00</td></tr>
<tr><th scope="row">INV-1043</th><td>Contoso</td><td class="wu-table-num">$86.50</td></tr>
<tr><th scope="row">INV-1044</th><td>Fabrikam</td><td class="wu-table-num">$12,940.00</td></tr>
</tbody>
</table>

A wu-table-sort button in the header cell shows both arrows dimmed; aria-sort on the cell shows one. Sorting the rows and updating aria-sort is up to your script.

Ana LopezOwner12
Ben OkaforEditor7
Chen WeiViewer3
<table class="wu-table">
<thead>
<tr>
<th scope="col" aria-sort="ascending"><button class="wu-table-sort" type="button">Name</button></th>
<th scope="col"><button class="wu-table-sort" type="button">Role</button></th>
<th scope="col" class="wu-table-num"><button class="wu-table-sort" type="button">Projects</button></th>
</tr>
</thead>
<tbody>
<tr><td>Ana Lopez</td><td>Owner</td><td class="wu-table-num">12</td></tr>
<tr><td>Ben Okafor</td><td>Editor</td><td class="wu-table-num">7</td></tr>
<tr><td>Chen Wei</td><td>Viewer</td><td class="wu-table-num">3</td></tr>
</tbody>
</table>
RegionOrdersRevenue
North320$41,200
South214$27,850
East198$24,100
West275$35,640
<table class="wu-table sm striped hover">
<thead>
<tr><th scope="col">Region</th><th scope="col" class="wu-table-num">Orders</th><th scope="col" class="wu-table-num">Revenue</th></tr>
</thead>
<tbody>
<tr><td>North</td><td class="wu-table-num">320</td><td class="wu-table-num">$41,200</td></tr>
<tr><td>South</td><td class="wu-table-num">214</td><td class="wu-table-num">$27,850</td></tr>
<tr><td>East</td><td class="wu-table-num">198</td><td class="wu-table-num">$24,100</td></tr>
<tr><td>West</td><td class="wu-table-num">275</td><td class="wu-table-num">$35,640</td></tr>
</tbody>
</table>

A wu-table-select cell holds a checkbox. A checked box selects its row: the row keeps its tint on a striped row and under the pointer, and a bar marks it at the start. The header box selects or clears every row, and the script shows it as mixed when only some rows are selected.

NameRoleProjects
Ana LopezOwner12
Ben OkaforEditor7
Chen WeiViewer3
<table class="wu-table striped hover">
<thead>
<tr>
<th scope="col" class="wu-table-select"><input type="checkbox" class="wu-checkbox-input" aria-label="Select all rows"></th>
<th scope="col">Name</th><th scope="col">Role</th><th scope="col" class="wu-table-num">Projects</th>
</tr>
</thead>
<tbody>
<tr><td class="wu-table-select"><input type="checkbox" class="wu-checkbox-input" aria-label="Select Ana Lopez"></td><td>Ana Lopez</td><td>Owner</td><td class="wu-table-num">12</td></tr>
<tr><td class="wu-table-select"><input type="checkbox" class="wu-checkbox-input" aria-label="Select Ben Okafor" checked></td><td>Ben Okafor</td><td>Editor</td><td class="wu-table-num">7</td></tr>
<tr><td class="wu-table-select"><input type="checkbox" class="wu-checkbox-input" aria-label="Select Chen Wei"></td><td>Chen Wei</td><td>Viewer</td><td class="wu-table-num">3</td></tr>
</tbody>
</table>

Listen for change on the table and read the checked boxes. Code that sets checked itself should then dispatch a bubbling change event on the box, so the header box follows.

A wu-table-expand button toggles the wu-table-detail row right after its row. The closed chevron points to the inline end, the open one points down. With animated, the chevron turns.

OrderCustomerTotal
3021 Northwind$320.00
3022 Contoso$86.50
One desk lamp, delivered on 4 March.
<table class="wu-table animated">
<thead>
<tr><th scope="col">Order</th><th scope="col">Customer</th><th scope="col" class="wu-table-num">Total</th></tr>
</thead>
<tbody>
<tr>
<th scope="row"><button class="wu-table-expand" type="button" aria-expanded="false" aria-label="Items in order 3021"></button>3021</th>
<td>Northwind</td><td class="wu-table-num">$320.00</td>
</tr>
<tr class="wu-table-detail" hidden><td colspan="3">Two chairs and a desk, shipped from the Leeds warehouse.</td></tr>
<tr>
<th scope="row"><button class="wu-table-expand" type="button" aria-expanded="true" aria-label="Items in order 3022"></button>3022</th>
<td>Contoso</td><td class="wu-table-num">$86.50</td>
</tr>
<tr class="wu-table-detail"><td colspan="3">One desk lamp, delivered on 4 March.</td></tr>
</tbody>
</table>

The script finds the detail row through aria-controls, or takes the next row, and sets aria-controls, aria-expanded and hidden.

wu-table-truncate keeps a long text on one line with an ellipsis. Put the text in a child element and the full text in core’s data-tooltip on the cell; tabindex="0" lets keyboard users open the tooltip too.

TicketSummaryStatus
T-118 Typo on the billing page Open
T-119 Export to CSV drops the last row when the report has more than one page Closed
<table class="wu-table">
<thead>
<tr><th scope="col">Ticket</th><th scope="col">Summary</th><th scope="col">Status</th></tr>
</thead>
<tbody>
<tr>
<td>T-118</td>
<td class="wu-table-truncate" tabindex="0" data-tooltip="Typo on the billing page" data-tooltip-size="large" style="--wu-table--TruncateWidth: 7rem"><span>Typo on the billing page</span></td>
<td>Open</td>
</tr>
<tr>
<td>T-119</td>
<td class="wu-table-truncate" tabindex="0" data-tooltip="Export to CSV drops the last row when the report has more than one page" data-tooltip-size="large" style="--wu-table--TruncateWidth: 7rem"><span>Export to CSV drops the last row when the report has more than one page</span></td>
<td>Closed</td>
</tr>
</tbody>
</table>

Rows in <tfoot> are semibold, with a strong line above them.

ItemQtyAmount
Hosting1$49.00
Extra seats3$36.00
Total4$85.00
<table class="wu-table">
<thead>
<tr><th scope="col">Item</th><th scope="col" class="wu-table-num">Qty</th><th scope="col" class="wu-table-num">Amount</th></tr>
</thead>
<tbody>
<tr><td>Hosting</td><td class="wu-table-num">1</td><td class="wu-table-num">$49.00</td></tr>
<tr><td>Extra seats</td><td class="wu-table-num">3</td><td class="wu-table-num">$36.00</td></tr>
</tbody>
<tfoot>
<tr><th scope="row">Total</th><td class="wu-table-num">4</td><td class="wu-table-num">$85.00</td></tr>
</tfoot>
</table>
PlanSeatsPrice
Starter1$0
Team10$49
<table class="wu-table bordered">
<thead>
<tr><th scope="col">Plan</th><th scope="col">Seats</th><th scope="col" class="wu-table-num">Price</th></tr>
</thead>
<tbody>
<tr><td>Starter</td><td>1</td><td class="wu-table-num">$0</td></tr>
<tr><td>Team</td><td>10</td><td class="wu-table-num">$49</td></tr>
</tbody>
</table>

A scroll region must be reachable by keyboard: give it tabindex="0", role="region" and an aria-label. With --wu-table--MaxHeight set, the region scrolls vertically too and a sticky-header table keeps its header in view.

<div class="wu-table-scroll" tabindex="0" role="region" aria-label="Monthly usage" style="--wu-table--MaxHeight: 12rem">
<table class="wu-table sticky-header">
<thead>
<tr><th scope="col">Month</th><th scope="col" class="wu-table-num">API calls</th><th scope="col" class="wu-table-num">Storage</th><th scope="col" class="wu-table-num">Bandwidth</th><th scope="col" class="wu-table-num">Build minutes</th><th scope="col" class="wu-table-num">Seats</th></tr>
</thead>
<tbody>
<tr><td>January</td><td class="wu-table-num">1,204,331</td><td class="wu-table-num">48 GB</td><td class="wu-table-num">1.2 TB</td><td class="wu-table-num">3,120</td><td class="wu-table-num">8</td></tr>
<tr><td>February</td><td class="wu-table-num">1,309,870</td><td class="wu-table-num">51 GB</td><td class="wu-table-num">1.4 TB</td><td class="wu-table-num">3,410</td><td class="wu-table-num">8</td></tr>
<tr><td>March</td><td class="wu-table-num">1,422,019</td><td class="wu-table-num">55 GB</td><td class="wu-table-num">1.5 TB</td><td class="wu-table-num">3,980</td><td class="wu-table-num">9</td></tr>
<tr><td>April</td><td class="wu-table-num">1,501,442</td><td class="wu-table-num">58 GB</td><td class="wu-table-num">1.6 TB</td><td class="wu-table-num">4,050</td><td class="wu-table-num">10</td></tr>
<tr><td>May</td><td class="wu-table-num">1,630,005</td><td class="wu-table-num">62 GB</td><td class="wu-table-num">1.8 TB</td><td class="wu-table-num">4,420</td><td class="wu-table-num">10</td></tr>
<tr><td>June</td><td class="wu-table-num">1,712,860</td><td class="wu-table-num">66 GB</td><td class="wu-table-num">1.9 TB</td><td class="wu-table-num">4,700</td><td class="wu-table-num">11</td></tr>
</tbody>
</table>
</div>

In a scroll region, sticky-start keeps the first column in view and sticky-end the last, such as a name column and an actions column. A shadow shows on the column’s inner edge while content is hidden under it, on the correct side in both directions. With a selection column, sticky-start keeps the checkboxes in view. A selected row’s bar and a truncated cell’s ellipsis follow the text direction too.

שםאימיילצוותמיקוםפרויקטיםפעולות
דנה לוי[email protected]הנדסת פלטפורמהליסבון, פורטוגל12
בן כהן[email protected]הצלחת לקוחותלגוס, ניגריה7
חן וייס[email protected]נתונים וניתוחסינגפור3
<div class="wu-table-scroll" tabindex="0" role="region" aria-label="חברי הצוות">
<table class="wu-table sticky-start sticky-end">
<thead>
<tr><th scope="col">שם</th><th scope="col">אימייל</th><th scope="col">צוות</th><th scope="col">מיקום</th><th scope="col" class="wu-table-num">פרויקטים</th><th scope="col">פעולות</th></tr>
</thead>
<tbody>
<tr><th scope="row">דנה לוי</th><td>[email protected]</td><td>הנדסת פלטפורמה</td><td>ליסבון, פורטוגל</td><td class="wu-table-num">12</td><td><button class="wu-btn sm" type="button">עריכה</button></td></tr>
<tr><th scope="row">בן כהן</th><td>[email protected]</td><td>הצלחת לקוחות</td><td>לגוס, ניגריה</td><td class="wu-table-num">7</td><td><button class="wu-btn sm" type="button">עריכה</button></td></tr>
<tr><th scope="row">חן וייס</th><td>[email protected]</td><td>נתונים וניתוח</td><td>סינגפור</td><td class="wu-table-num">3</td><td><button class="wu-btn sm" type="button">עריכה</button></td></tr>
</tbody>
</table>
</div>
  • aria-selected on a <tr> is valid only in a role="grid" or role="treegrid" table, which also needs grid keyboard navigation. In a plain table the checkbox carries the selection, so screen readers announce it: label each box with its row (Select Ana Lopez) and the header box Select all rows. The script sets the header box to checked, mixed or clear, and in a role="grid" table it also sets aria-selected on each row.
  • Give each expand button an aria-label that names its row. The script sets aria-expanded and aria-controls.
  • Screen readers read the full text of a truncated cell. A scroll region clips tooltips at its edges.
  • A scroll region must be reachable by keyboard: tabindex="0", role="region" and an aria-label.
@use "@wovenui/components/table";

The table needs no other component.