Data and lists¶
Business applications are mostly forms and tables. Forms has the first half; this is the second.
Tables¶
<div class="ads-table-scroll">
<table class="ads-table ads-table--vcenter ads-table--hover">
<thead>
<tr>
<th><button class="ads-th-sort" aria-sort="ascending">Name</button></th>
<th>Company</th>
</tr>
</thead>
<tbody>
<tr><td>Amélie Rousseau</td><td class="ads-muted">Northwind</td></tr>
</tbody>
</table>
</div>
Modifiers: --vcenter, --hover, --striped, --compact, --sticky (a header that stays
while the body scrolls), --selectable.
.ads-table-scroll is not optional
Without it a wide table scrolls the page sideways instead of itself, and the far
columns become unreachable at 320px and at 200% text. abilian-ds lint reports this as
table-overflow.
It used to switch itself off above 62em, so a row menu could escape it. That was wrong
twice over — see Accessibility. The clipping is now lifted only
while a menu inside the table is actually open, with :has(details[open]).
Sorting¶
ads-th-sort is a <button>, not a link with a decoration, and it carries aria-sort:
<th><button class="ads-th-sort" aria-sort="ascending">Name</button></th>
<th><button class="ads-th-sort" aria-sort="descending">Created</button></th>
<th><button class="ads-th-sort">Company</button></th> <!-- unsorted -->
The arrow is drawn from the attribute, so a sorted column cannot look sorted without saying so.
Selection, and the bar that follows it¶
<div class="ads-datatable">
<div class="ads-card__head ads-datatable__idle ads-spread">…the usual toolbar…</div>
<div class="ads-bulkbar">
<strong>3</strong> selected
<div class="ads-cluster ads-push">
<button class="ads-btn ads-btn--sm">Close</button>
</div>
</div>
<div class="ads-table-scroll">
<table class="ads-table ads-table--selectable">
<tbody>
<tr><td class="ads-table__select"><input type="checkbox" aria-label="Select Bea"></td>…</tr>
</tbody>
</table>
</div>
</div>
Inside .ads-datatable you write no state at all: :has(tbody input:checked) swaps the
toolbar for the bar and tints the selected rows. That is the whole mechanism.
Outside a table — a list, a board, a grid of cards, anywhere the selection is a set in your
own state — use ads-bulkbar on its own and hide it with the hidden attribute:
Why hidden and not a class
It is the platform's answer for not displayed, and base.css carries
[hidden] { display: none !important } so it wins over any component's own display.
That rule matters more than it looks: without it, hidden loses to every author
display, and a framework that toggles hidden — situ's :show does — silently fails
on half the system.
This component was .ads-datatable__bulk and revealable only by the table's :has()
rule, which made it unusable anywhere else. See note 23.
Lists¶
A row that is entirely a link takes the list's own ink, not the link colour — blue-and- underlined is for a link inside a sentence, where it has to be told apart from the words either side. A whole row has nothing to be told apart from.
A row that selects without navigating is a <button>, and is styled the same way:
aria-current marks the selected row, and the stylesheet keys off it, so the highlight cannot
exist without the semantics. Only a row you can pick lights up under the pointer, and the
selected one keeps its own tint while you hover it.
ads-list--plain gives back the inline padding when the list sits in a card body that already
has it — on every row except the ones you can pick. A picked row keeps its padding: the hover
tint, the selected tint and the 3px marker have nowhere to land without it. It gives back
nothing vertically, because a row's block padding is the row's and whatever sits above the list
is the stack's business.
A row can also hold an action — a star, a delete. Write it as an ads-btn and it keeps its own
size; a bare <button> is read as the row itself and takes the width.
Hierarchies¶
<ul class="ads-tree">
<li class="ads-tree__node">
<span class="ads-tree__label">Engineering</span>
<ul class="ads-tree ads-tree--nested">
<li class="ads-tree__node"><span class="ads-tree__label">Compiler</span></li>
</ul>
</li>
</ul>
The guide rule lives on the nested list, not on each node, so it is one line per level rather than one per row and it survives rows being added or removed. Labels dim one step per level, cumulatively, so depth reads without a class per depth.
Long lists¶
When the list is too long to put in the document at all:
<div class="ads-viewport" style="--pane: 60vh">
<div class="ads-viewport__sizer" style="--extent: 560000px">
<div class="ads-viewport__row" style="--row: 28px">…</div>
</div>
</div>
Three numbers, and all three are data rather than design: how tall the pane is, how tall
the whole list would be, and how tall one row is. A custom property is the one thing the
system lets style= carry, for exactly this reason.
Three names, not one --h
Custom properties inherit. A single name would have every row silently taking the pane's height the moment one row forgot to set its own.
A virtual scroller computes its window from the row height, so the CSS and the server have to agree on that number — keep them next to each other.
Empty states¶
<div class="ads-empty">
<p class="ads-empty__title">No issues</p>
<p class="ads-empty__subtitle">Create one, or widen the filter.</p>
</div>
Every list in this system has one. A table that renders zero rows and no explanation is the most common unfinished edge in a business application.