RZREDZ.UI

Tables

A list of records: a title that links to the record, a line under it, a note when something is wrong, a column of checks for batch actions and the row's own actions at the end.

Examples

table on the <table>; the row under the pointer lights on a device with one, in the color of --redz-row-hover, set on the page or on one table, and table-static keeps a table's rows still. The head stays at the top of the screen as the page scrolls. Columns that only help on a wide screen hide below md of their card.

Excursions

3 of 28
Excursion Price from Status Actions
North Bay: marine park by speedboat
/excursions/north-bay/
2,500 THB Published
Elephant sanctuary: feeding and a walk
Did not rebuild: a PHP warning in the theme template
2,600 THB Failed
Night market and street food
The Thai translation is older than the English
900 THB Stale
Folk dance: an evening at the stadium
withdrawn from the sites
1,500 THB Withdrawn
<section class="card" aria-labelledby="table-list-title">
	<div class="card-header"><h2 id="table-list-title">Excursions</h2><span class="text-muted fs-12">3 of 28</span></div>
	<div class="card-body p-0">
		<table class="table">
			<thead>
				<tr>
					<th class="table-check"><input class="check" type="checkbox" aria-label="Select all"></th>
					<th>Excursion</th>
					<th class="text-end d-none md:d-table-cell">Price from</th>
					<th class="d-none md:d-table-cell">Status</th>
					<th class="table-actions"><span class="visually-hidden">Actions</span></th>
				</tr>
			</thead>
			<tbody>
				<tr>
					<td><input class="check" type="checkbox" aria-label="Select: North Bay"></td>
					<td><a class="table-title" href="#table-list">North Bay: marine park by speedboat</a><div class="table-sub d-none md:d-block">/excursions/north-bay/</div></td>
					<td class="text-end tabular-nums text-nowrap d-none md:d-table-cell">2,500 THB</td>
					<td class="d-none md:d-table-cell"><span class="badge"><i class="bi bi-check2" aria-hidden="true"></i>Published</span></td>
					<td class="table-actions"><button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Edit: North Bay"><i class="bi bi-pencil" aria-hidden="true"></i></button></td>
				</tr>
				<tr class="table-danger">
					<td><input class="check" type="checkbox" aria-label="Select: Elephant sanctuary"></td>
					<td><a class="table-title" href="#table-list">Elephant sanctuary: feeding and a walk</a><div class="table-note"><i class="bi bi-x-octagon-fill" aria-hidden="true"></i><span class="text-muted">Did not rebuild: a PHP warning in the theme template</span></div></td>
					<td class="text-end tabular-nums text-nowrap d-none md:d-table-cell">2,600 THB</td>
					<td class="d-none md:d-table-cell"><span class="badge badge-subtle-danger"><i class="bi bi-x-octagon" aria-hidden="true"></i>Failed</span></td>
					<td class="table-actions"><button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Rebuild: Elephant sanctuary"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button></td>
				</tr>
				<tr class="table-warning">
					<td><input class="check" type="checkbox" aria-label="Select: Night market"></td>
					<td><a class="table-title" href="#table-list">Night market and street food</a><div class="table-note"><i class="bi bi-exclamation-triangle-fill" aria-hidden="true"></i><span class="text-muted">The Thai translation is older than the English</span></div></td>
					<td class="text-end tabular-nums text-nowrap d-none md:d-table-cell">900 THB</td>
					<td class="d-none md:d-table-cell"><span class="badge badge-subtle-warning"><i class="bi bi-clock-history" aria-hidden="true"></i>Stale</span></td>
					<td class="table-actions"><button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Edit: Night market"><i class="bi bi-pencil" aria-hidden="true"></i></button></td>
				</tr>
				<tr class="table-secondary">
					<td><input class="check" type="checkbox" aria-label="Select: Folk dance"></td>
					<td><a class="table-title" href="#table-list">Folk dance: an evening at the stadium</a><div class="table-sub d-none md:d-block">withdrawn from the sites</div></td>
					<td class="text-end tabular-nums text-nowrap d-none md:d-table-cell">1,500 THB</td>
					<td class="d-none md:d-table-cell"><span class="badge"><i class="bi bi-eye-slash" aria-hidden="true"></i>Withdrawn</span></td>
					<td class="table-actions"><button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Edit: Folk dance"><i class="bi bi-pencil" aria-hidden="true"></i></button></td>
				</tr>
			</tbody>
		</table>
	</div>
</section>

Stripes, borders and groups

table-striped shades every other row, table-striped-columns every other column - under the hover's light and a row's tone alike; table-group-divider on a tbody draws a stronger line above it. table-bordered draws the lines between columns and, outside a card, the table's own frame; table-borderless takes the lines between rows away.

Boats this week

BoatTripsSeats sold
Boat A12286
Boat C9201
Boat B7164
All boats28651
SeasonMonthsThe sea
DryJanuary to AprilCalm
HotMay to AugustCalm
RainySeptember to DecemberWaves
<section class="card" aria-labelledby="table-variants-title">
	<div class="card-header"><h2 class="fs-14" id="table-variants-title">Boats this week</h2></div>
	<table class="table table-striped">
		<thead><tr><th scope="col">Boat</th><th class="text-end" scope="col">Trips</th><th class="text-end" scope="col">Seats sold</th></tr></thead>
		<tbody>
			<tr><td>Boat A</td><td class="text-end tabular-nums">12</td><td class="text-end tabular-nums">286</td></tr>
			<tr><td>Boat C</td><td class="text-end tabular-nums">9</td><td class="text-end tabular-nums">201</td></tr>
			<tr><td>Boat B</td><td class="text-end tabular-nums">7</td><td class="text-end tabular-nums">164</td></tr>
		</tbody>
		<tbody class="table-group-divider">
			<tr><td>All boats</td><td class="text-end tabular-nums">28</td><td class="text-end tabular-nums">651</td></tr>
		</tbody>
	</table>
</section>
<table class="table table-bordered" aria-label="Seasons on Main Island">
	<thead><tr><th scope="col">Season</th><th scope="col">Months</th><th scope="col">The sea</th></tr></thead>
	<tbody>
		<tr><td>Dry</td><td>January to April</td><td>Calm</td></tr>
		<tr><td>Hot</td><td>May to August</td><td>Calm</td></tr>
		<tr><td>Rainy</td><td>September to December</td><td>Waves</td></tr>
	</tbody>
</table>

Row sizes

data-density="compact" tightens the rows and the controls of the element it is on and of everything inside it: a table, the card that holds it, a region, or the whole page on <html>. A compact table can stand beside a normal one on the same screen. Bootstrap 5 calls it table-sm. data-density="comfortable" and data-density="spacious" open the rows up the other way, for a short table that is the point of a page.

SeasonMonthsThe sea
DryJanuary to AprilCalm
HotMay to AugustCalm
RainySeptember to DecemberWaves
SeasonMonthsThe sea
DryJanuary to AprilCalm
HotMay to AugustCalm
<table class="table table-bordered" data-density="compact" aria-label="Seasons on Main Island, compact">
	<thead><tr><th scope="col">Season</th><th scope="col">Months</th><th scope="col">The sea</th></tr></thead>
	<tbody>
		<tr><td>Dry</td><td>January to April</td><td>Calm</td></tr>
		<tr><td>Hot</td><td>May to August</td><td>Calm</td></tr>
		<tr><td>Rainy</td><td>September to December</td><td>Waves</td></tr>
	</tbody>
</table>
<table class="table table-bordered" data-density="spacious" aria-label="Seasons on Main Island, spacious">
	<thead><tr><th scope="col">Season</th><th scope="col">Months</th><th scope="col">The sea</th></tr></thead>
	<tbody>
		<tr><td>Dry</td><td>January to April</td><td>Calm</td></tr>
		<tr><td>Hot</td><td>May to August</td><td>Calm</td></tr>
	</tbody>
</table>

Rows that stay still

Every table lights the row under the pointer. table-static keeps the rows of one still, for a table with nothing to do in a row.

North Bay08:00
Turtle Cove10:30
Adult1,200
Child600
<table class="table" aria-label="Departures">
	<tbody>
		<tr><td>North Bay</td><td>08:00</td></tr>
		<tr><td>Turtle Cove</td><td>10:30</td></tr>
	</tbody>
</table>
<table class="table table-static" aria-label="Fares">
	<tbody>
		<tr><td>Adult</td><td>1,200</td></tr>
		<tr><td>Child</td><td>600</td></tr>
	</tbody>
</table>

A menu in a row

A row's menu stands in table-actions right after its button. While it is open the row keeps its light and its actions, wherever the pointer goes, and the menu takes nothing from the cell. Rows may also share one menu kept outside the table: each button names it, and the row of the button that opened it stays lit.

ExcursionLeavesActions
North Bay 08:00
Turtle Cove 10:30
<table class="table" aria-label="Departures">
	<thead>
		<tr><th scope="col">Excursion</th><th scope="col">Leaves</th><th class="table-actions"><span class="visually-hidden">Actions</span></th></tr>
	</thead>
	<tbody>
		<tr>
			<td>North Bay</td>
			<td>08:00</td>
			<td class="table-actions">
				<button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Edit: North Bay"><i class="bi bi-pencil" aria-hidden="true"></i></button>
				<button class="btn btn-sm btn-icon btn-text" type="button" commandfor="table-row-menu-bay" command="toggle-popover" aria-haspopup="menu" aria-label="More: North Bay"><i class="bi bi-three-dots" aria-hidden="true"></i></button>
				<div id="table-row-menu-bay" class="menu" popover role="menu" aria-label="More: North Bay">
					<button class="menu-item" type="button" role="menuitem"><i class="bi bi-files" aria-hidden="true"></i>Duplicate</button>
					<button class="menu-item" type="button" role="menuitem"><i class="bi bi-eye-slash" aria-hidden="true"></i>Withdraw</button>
				</div>
			</td>
		</tr>
		<tr>
			<td>Turtle Cove</td>
			<td>10:30</td>
			<td class="table-actions">
				<button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Edit: Turtle Cove"><i class="bi bi-pencil" aria-hidden="true"></i></button>
				<button class="btn btn-sm btn-icon btn-text" type="button" commandfor="table-row-menu-cove" command="toggle-popover" aria-haspopup="menu" aria-label="More: Turtle Cove"><i class="bi bi-three-dots" aria-hidden="true"></i></button>
				<div id="table-row-menu-cove" class="menu" popover role="menu" aria-label="More: Turtle Cove">
					<button class="menu-item" type="button" role="menuitem"><i class="bi bi-files" aria-hidden="true"></i>Duplicate</button>
					<button class="menu-item" type="button" role="menuitem"><i class="bi bi-eye-slash" aria-hidden="true"></i>Withdraw</button>
				</div>
			</td>
		</tr>
	</tbody>
</table>

Rows that light together

Elements that share a data-highlight key light together: hovering or focusing one marks every element with its key data-highlighted, tinted - a row here and its lane on a board, or its marker on a map. A mark the page sets itself, data-highlighted="hold", stays until the page takes it away: a row flashed after a jump to it, say.

The pier08:30
The emerald lake11:00
<div class="hstack flex-wrap gap-8">
	<button class="btn btn-sm" type="button" data-highlight="pier">The pier</button>
	<button class="btn btn-sm" type="button" data-highlight="lake">The emerald lake</button>
</div>
<table class="table" aria-label="Stops">
	<tbody>
		<tr data-highlight="pier"><td>The pier</td><td class="tabular-nums">08:30</td></tr>
		<tr data-highlight="lake"><td>The emerald lake</td><td class="tabular-nums">11:00</td></tr>
	</tbody>
</table>

Sortable columns

A header that sorts holds a button class="table-sort", and the header's aria-sort - ascending, descending or none - draws the arrow. The page sorts - on the server or in place - and moves aria-sort to the column it sorted by.

Example Resort348,000
Example Cruises19,600
Example Divers221,300
<table class="table" aria-label="Owners">
	<thead><tr><th scope="col" aria-sort="ascending"><button class="table-sort" type="button">Name</button></th><th scope="col"><button class="table-sort" type="button">Sites</button></th><th scope="col" class="text-end"><button class="table-sort" type="button">Paid</button></th></tr></thead>
	<tbody>
		<tr><td>Example Resort</td><td class="tabular-nums">3</td><td class="text-end tabular-nums">48,000</td></tr>
		<tr><td>Example Cruises</td><td class="tabular-nums">1</td><td class="text-end tabular-nums">9,600</td></tr>
		<tr><td>Example Divers</td><td class="tabular-nums">2</td><td class="text-end tabular-nums">21,300</td></tr>
	</tbody>
</table>

Where the server sorts, the header holds a link instead - a class="table-sort" to the same list in the other order - drawn the same way.

<table class="table" aria-label="Owners, sorted by the server">
	<thead><tr><th scope="col"><a class="table-sort" href="#table-sort-links">Name</a></th><th scope="col" class="text-end" aria-sort="descending"><a class="table-sort" href="#table-sort-links">Paid</a></th></tr></thead>
	<tbody>
		<tr><td>Example Resort</td><td class="text-end tabular-nums">48,000</td></tr>
		<tr><td>Example Divers</td><td class="text-end tabular-nums">21,300</td></tr>
	</tbody>
</table>

Groups and subtotals

A row table-group heads the rows under it: one th scope="rowgroup" across the table, a band that does not stick, with an end slot table-group-end for its count or sum. A row table-subtotal closes a group in bold under a line.

GuestSeatsPaid
Boat A2 bookings
Alex Sample25,000
Ben Sample37,500
Boat A512,500
Boat C1 booking
Chris Sample410,000
Boat C410,000
<table class="table" aria-label="Bookings by boat">
	<thead><tr><th scope="col">Guest</th><th scope="col" class="text-end">Seats</th><th scope="col" class="text-end">Paid</th></tr></thead>
	<tbody>
		<tr class="table-group"><th scope="rowgroup" colspan="3">Boat A<span class="table-group-end">2 bookings</span></th></tr>
		<tr><td>Alex Sample</td><td class="text-end tabular-nums">2</td><td class="text-end tabular-nums">5,000</td></tr>
		<tr><td>Ben Sample</td><td class="text-end tabular-nums">3</td><td class="text-end tabular-nums">7,500</td></tr>
		<tr class="table-subtotal"><th scope="row">Boat A</th><td class="text-end tabular-nums">5</td><td class="text-end tabular-nums">12,500</td></tr>
		<tr class="table-group"><th scope="rowgroup" colspan="3">Boat C<span class="table-group-end">1 booking</span></th></tr>
		<tr><td>Chris Sample</td><td class="text-end tabular-nums">4</td><td class="text-end tabular-nums">10,000</td></tr>
		<tr class="table-subtotal"><th scope="row">Boat C</th><td class="text-end tabular-nums">4</td><td class="text-end tabular-nums">10,000</td></tr>
	</tbody>
</table>

The head of a table sticks under the app's header as the page scrolls; inside a region that scrolls on its own - a dialog-body, a card-scroll card's body - it sticks to the region's top.

Wider than its card

A table with more columns than its card has room for goes in a table-scroll: it scrolls sideways inside the card, and table-scroll-first holds its first column in place, table-scroll-last its last, where a row's buttons stay in view. Where the browser lets an element scroll on one axis alone - Chrome 156 on - the head still sticks to the page as it scrolls down; elsewhere the head scrolls away with the page and the rest works the same. The wrapper is a named region. The library makes it a tab stop while its table is wider than it, so a keyboard can scroll it, and takes the stop away while the table fits.

Seats by day

BoatMonTueWedThuFriSatSunWeekLast weekChange
Boat A42384044464828286270+16
Boat C30282731333517201214-13
Boat B22202423253020164150+14
<section class="card" aria-labelledby="table-wide-title">
	<div class="card-header"><h2 class="fs-14" id="table-wide-title">Seats by day</h2></div>
	<div class="table-scroll table-scroll-first" role="region" aria-labelledby="table-wide-title">
		<table class="table">
			<thead><tr><th scope="col">Boat</th><th scope="col" class="text-end">Mon</th><th scope="col" class="text-end">Tue</th><th scope="col" class="text-end">Wed</th><th scope="col" class="text-end">Thu</th><th scope="col" class="text-end">Fri</th><th scope="col" class="text-end">Sat</th><th scope="col" class="text-end">Sun</th><th scope="col" class="text-end">Week</th><th scope="col" class="text-end">Last week</th><th scope="col" class="text-end">Change</th></tr></thead>
			<tbody>
				<tr><th scope="row">Boat A</th><td class="text-end tabular-nums">42</td><td class="text-end tabular-nums">38</td><td class="text-end tabular-nums">40</td><td class="text-end tabular-nums">44</td><td class="text-end tabular-nums">46</td><td class="text-end tabular-nums">48</td><td class="text-end tabular-nums">28</td><td class="text-end tabular-nums">286</td><td class="text-end tabular-nums">270</td><td class="text-end tabular-nums"><bdi>+16</bdi></td></tr>
				<tr><th scope="row">Boat C</th><td class="text-end tabular-nums">30</td><td class="text-end tabular-nums">28</td><td class="text-end tabular-nums">27</td><td class="text-end tabular-nums">31</td><td class="text-end tabular-nums">33</td><td class="text-end tabular-nums">35</td><td class="text-end tabular-nums">17</td><td class="text-end tabular-nums">201</td><td class="text-end tabular-nums">214</td><td class="text-end tabular-nums"><bdi>-13</bdi></td></tr>
				<tr><th scope="row">Boat B</th><td class="text-end tabular-nums">22</td><td class="text-end tabular-nums">20</td><td class="text-end tabular-nums">24</td><td class="text-end tabular-nums">23</td><td class="text-end tabular-nums">25</td><td class="text-end tabular-nums">30</td><td class="text-end tabular-nums">20</td><td class="text-end tabular-nums">164</td><td class="text-end tabular-nums">150</td><td class="text-end tabular-nums"><bdi>+14</bdi></td></tr>
			</tbody>
		</table>
	</div>
</section>

Rates

BoatLowHighPeakChildrenGroupsAgentsGuidesFuelPark feeInsuranceStatus
Boat A2,5002,9003,4001,9002,2002,100900300300120
Boat C2,2002,6003,0001,7002,0001,900800280300110
<section class="card" aria-labelledby="table-wide-last-title">
	<div class="card-header"><h2 class="fs-14" id="table-wide-last-title">Rates</h2></div>
	<div class="table-scroll table-scroll-last" role="region" aria-labelledby="table-wide-last-title">
		<table class="table">
			<thead><tr><th scope="col">Boat</th><th scope="col" class="text-end">Low</th><th scope="col" class="text-end">High</th><th scope="col" class="text-end">Peak</th><th scope="col" class="text-end">Children</th><th scope="col" class="text-end">Groups</th><th scope="col" class="text-end">Agents</th><th scope="col" class="text-end">Guides</th><th scope="col" class="text-end">Fuel</th><th scope="col" class="text-end">Park fee</th><th scope="col" class="text-end">Insurance</th><th scope="col" class="text-end">Status</th></tr></thead>
			<tbody>
				<tr><th scope="row">Boat A</th><td class="text-end tabular-nums">2,500</td><td class="text-end tabular-nums">2,900</td><td class="text-end tabular-nums">3,400</td><td class="text-end tabular-nums">1,900</td><td class="text-end tabular-nums">2,200</td><td class="text-end tabular-nums">2,100</td><td class="text-end tabular-nums">900</td><td class="text-end tabular-nums">300</td><td class="text-end tabular-nums">300</td><td class="text-end tabular-nums">120</td><td class="text-end"><button class="btn btn-xs btn-primary" type="button">Save</button></td></tr>
				<tr><th scope="row">Boat C</th><td class="text-end tabular-nums">2,200</td><td class="text-end tabular-nums">2,600</td><td class="text-end tabular-nums">3,000</td><td class="text-end tabular-nums">1,700</td><td class="text-end tabular-nums">2,000</td><td class="text-end tabular-nums">1,900</td><td class="text-end tabular-nums">800</td><td class="text-end tabular-nums">280</td><td class="text-end tabular-nums">300</td><td class="text-end tabular-nums">110</td><td class="text-end"><button class="btn btn-xs btn-primary" type="button">Save</button></td></tr>
			</tbody>
		</table>
	</div>
</section>

A product sets the look of every table head in its tokens: --redz-table-head-bg, --redz-table-head-text, --redz-table-head-weight, and --redz-table-head-border, the whole line under it.

The current, the chosen and the running row

A row with aria-current - the record open beside the list - carries the accent's mark at its start; a row with aria-selected="true", chosen in a grid of rows, is tinted; table-active, Bootstrap's name, marks the row that is running now.

Rebuild the Thai pagesOpen on the right
Import the price listChosen
Send the morning lettersRunning now
Clean the old photosWaiting
<table class="table" aria-label="Jobs">
	<tbody>
		<tr aria-current="true"><td>Rebuild the Thai pages</td><td class="text-muted">Open on the right</td></tr>
		<tr aria-selected="true"><td>Import the price list</td><td class="text-muted">Chosen</td></tr>
		<tr class="table-active"><td>Send the morning letters</td><td class="text-muted">Running now</td></tr>
		<tr><td>Clean the old photos</td><td class="text-muted">Waiting</td></tr>
	</tbody>
</table>

The row's title link with Bootstrap's stretched-link makes the whole row a link: a click anywhere on it but on its own controls and their cells (table-check, table-actions) follows the title - with Ctrl, Cmd or the middle button into a new tab - unless the person was selecting its text. The title stays the one link a keyboard and a screen reader meet. The library does it by script, as Safari gives a positioned row no box for the link's cover.

<table class="table" aria-label="Guides">
	<tbody>
		<tr><td><a class="table-title stretched-link" href="#table-links">Alex Sample</a><div class="table-sub">Thai, English</div></td><td class="tabular-nums">12 tours</td><td class="table-actions"><button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Message: Alex Sample"><i class="bi bi-chat" aria-hidden="true"></i></button></td></tr>
		<tr><td><a class="table-title stretched-link" href="#table-links">Hana Sample</a><div class="table-sub">Japanese, English</div></td><td class="tabular-nums">7 tours</td><td class="table-actions"><button class="btn btn-sm btn-icon btn-text" type="button" aria-label="Message: Hana Sample"><i class="bi bi-chat" aria-hidden="true"></i></button></td></tr>
	</tbody>
</table>

States

A row's tone marks what needs attention: table-danger failed, table-warning needs a look, table-secondary is out of use. On a pointer screen the row's actions show on hover and on focus; on a touch screen they always show.

rowrest:hover
tr
North Bay
North Bay
tr.table-danger
Elephant sanctuary
Elephant sanctuary
tr.table-warning
Night market
Night market
tr.table-secondary
Folk dance
Folk dance

Keyboard and accessibility

  • The row's title is a link; its actions are buttons named with the record - "Edit: North Bay", not a column of identical "Edit".
  • The column of actions has a hidden header, and each check its own name: "Select: North Bay".
  • A tone is never the only sign: a failed row also says why in its table-note, with an icon.
  • Numbers sit at the end of their cells with tabular-nums, so their digits line up.
  • A table being reloaded turns busy on its card's body: REDZ.ui.busy(body, promise) - not on the <table>, which cannot hold an overlay.

Tokens

From css/components/table.css. It declares --redz-table-head-border, --redz-table-head-weight, --redz-row-tint, --redz-row-mark on its root, never on rows or items.

It reads the theme's --redz-space-8, --redz-row-pad, --redz-space-12, --redz-border, --redz-table-head-bg, --redz-bg, --redz-table-head-text, --redz-text-muted, --redz-fs-12, --redz-text, --redz-bg-page, --redz-border-strong, --redz-row-hover, --redz-primary, --redz-radius-12, --redz-space-16, --redz-font-mono, --redz-space-4, --redz-danger-subtle, --redz-danger, --redz-warning-subtle, --redz-warning, --redz-primary-subtle.

From Bootstrap 5.3

Bootstrap 5.3REDZ.UIChangeNote
caption-top-missing
table-dark-reviewrow tones are danger, warning and secondary
table-hover-removeevery table lights its row under the pointer; table-static keeps one still
table-info-reviewrow tones are danger, warning and secondary
table-light-reviewrow tones are danger, warning and secondary
table-primary-reviewrow tones are danger, warning and secondary
table-responsivetable-scrollrenamethe wrapper scrolls sideways, table-scroll-first holds the first column; or columns hide below md:
table-success-reviewrow tones are danger, warning and secondary