busy-office-ui
Menu

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

ClassRole
.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)
TextonLightDark
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)
↑↓ move Enter open Esc close