Pagination
A <nav aria-label="Pagination"> of links/buttons; the current
page carries aria-current="page". Pairs with
.bo-data-table__footer under a table. Navigation is your code's job
(links or HTMX) — the framework styles state.
Not for stepping through a process — numbered page
links say "the same kind of thing continues, in arbitrary order, and
you may jump anywhere". A sequence where each step must be completed
before the next is a stepper
(or the wizard pattern), where
order is the point and skipping ahead is refused. The tell is whether
landing on page 7 first would be wrong.
Load more — the infinite-scroll alternative opt-in
| Invoice # | Amount |
|---|---|
| INV-10234 | $4,208.00 |
| INV-10235 | $18,940.50 |
When page numbers don't fit the interaction (activity
feeds, mobile lists): data-table-load-more +
initLoadMore() dispatches bo:table-load-more on click —
your code fetches the next batch and appends the rows (fetch, hx-get),
this behavior only tracks intent, same split as bo:table-export.
Add data-load-more-auto and an IntersectionObserver
fires the same event whenever the button is in view — once per approach, so a
consumer that fails to append doesn't loop. Note the boundary: an auto button
that is already in the viewport when initLoadMore() runs fires
straight away, with no scroll and no click, so a short first page fetches its
second page on load. Disable the button while a fetch is
in flight; remove it when there's nothing left. Try it: the demo appends two
stub rows per click.
<div class="bo-data-table__footer">
<button class="bo-btn bo-btn--secondary" type="button"
data-table-load-more data-load-more-auto>Load more</button>
</div>
<script type="module">
import { initLoadMore } from "@busy-office/ui/js";
initLoadMore(); // once — click + auto-fire while the button is in view
document.addEventListener("bo:table-load-more", async (e) => {
e.target.disabled = true; // in flight
const rows = await fetchNextPage(); // YOUR code
tbody.append(...rows);
e.target.disabled = false; // or remove it when exhausted
});
</script> Markup
<div class="bo-data-table__footer">
<label class="bo-choice">Rows
<select class="bo-select" aria-label="Rows per page">
<option>25</option><option>50</option><option>100</option>
</select>
</label>
<nav class="bo-pagination" aria-label="Pagination">
<button class="bo-pagination__btn" type="button" disabled aria-label="Previous page">‹</button>
<a class="bo-pagination__btn" aria-current="page" href="#">1</a>
<a class="bo-pagination__btn" href="#">2</a>
<a class="bo-pagination__btn" href="#">3</a>
<button class="bo-pagination__btn" type="button" aria-label="Next page">›</button>
<span class="bo-pagination__info">1–25 of 312</span>
</nav>
</div> Quick reference generated
| Class | Role |
|---|---|
| .bo-pagination | block (root) |
| .bo-pagination__btn | part: btn |
| .bo-pagination__info | part: info |
State attributes: aria-current
API generated from CSS
| Blocks | bo-pagination |
|---|---|
| Parts | bo-pagination__btnbo-pagination__info |
| ARIA styled | aria-current |
| JS required | Optional: initLoadMore() — dispatches bo:table-load-more on a [data-table-load-more] button (click, or while in view with data-load-more-auto, including immediately if it is already in view at init). Fetching/appending is your code. |
- Current page carries
aria-current="page"; navigation itself is your links/HTMX. - The table footer (
.bo-data-table__footer) is owned by data-table — import that file too when going à la carte. - Load more vs page numbers: load-more suits feeds and mobile lists; page numbers suit "jump to page 13" reference tables. Both compose with the same footer.
Contrast — 14 AA pairs this component uses (generated, CI-gated)
| Text | on | Light | Dark |
|---|---|---|---|
| text-primary | bg-surface | 17.74 | 16.15 |
| text-primary | bg-canvas | 16.98 | 18.08 |
| text-secondary | bg-surface | 10.31 | 11.46 |
| text-secondary | bg-muted | 9.37 | 9.75 |
| bg-surface | text-primary | 17.74 | 16.15 |
| accent-text | accent-subtle | 7.27 | 8.38 |
| text-primary | bg-hover | 16.12 | 13.75 |
| text-primary | bg-selected | 17.01 | 11.86 |
| text-secondary | bg-selected | 9.88 | 8.41 |
| accent-text | bg-selected | 7.27 | 8.38 |
| text-primary | bg-surface-raised | 17.74 | 14.51 |
| text-secondary | bg-canvas | 9.86 | 12.82 |
| accent | accent-subtle | 5.25 | 6.66 |
| text-primary | bg-muted | 16.12 | 13.75 |
Maturity generated
- Introduced
- v0.1.0
- CSS floor
- Chrome/Edge 99 · Firefox 97 · Safari 15.4
- Alignment scored
- 2026-08-23
- Assistive-tech evidence
- None recorded
Pin this component's first release with npm i @busy-office/ui@0.1.0.
The floor is derived from this component's stylesheet alone — set by
@layer — so it sits at or below the
whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5.
No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour.
What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see
accessibility.
Design-system alignment generated
100% (18/18) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-23.
| Rule | Score | Evidence |
|---|---|---|
| Typography | 3 / 3 | no raw font-size in pagination.css |
| Colour | 3 / 3 | zero raw hex |
| Spacing | 3 / 3 | zero raw dimension literals — all tokens |
| Interaction | 3 / 3 | native <nav> + buttons/links; current page carries aria-current='page' per its own contract comment |
| Content | 3 / 3 | opener carries the wrong-choice clause: "Not for stepping through a process" — numbered links mean arbitrary order and jump-anywhere; an ordered sequence is a stepper or the wizard pattern, with the tell stated (would landing on page 7 first be wrong). Blind re-scored 2026-08-23 (Polish round 10), judged genuine; the scorer noted the load-more section sits on the allowed side of the boundary rather than against it. |
| Fit | 3 / 3 | the documented answer at the scale where tables stop being scannable (/concepts/scale) |