Skip to content

BbPagination

A page selector for long lists that folds long ranges, fits its container and tells you which page to load.

import { BbPagination } from 'bitboss-ui';

On this page

Use it for

Reach for BbPagination when a list is too long for one screen and people need deterministic, jump-anywhere access — search results, an invoice ledger, an audit log. Prefer it over infinite scroll whenever someone has to find their place again, or refer to "page 4".

Use something else when

  • BbBreadcrumbs — the reader is moving through a hierarchy, not a sequence
  • BbTabs — they are peer views of the same record

BbPagination selects a page. Your application owns the page state, visible rows, request and loading feedback.

Passthrough

Hover or tap a part to outline it. The toggles turn on loading, errors and warnings where the component has them; only parts get a row.

Paginating an application list

Start with a real list. Bind the 1-based page, derive the visible rows from it, and pass the result count back to the control.

  • ORD-2026-0417Delivered€1,298.00
  • ORD-2026-0418Delivered€338.80
  • ORD-2026-0421Refunded€899.00
  • ORD-2026-0426Delivered€477.00
  • ORD-2026-0430Delivered€2,196.00

1–5 of 20

The list owns page, rows and loading. The pager emits only the next page. While the request runs, disabled prevents overlapping navigation and the list keeps the loading message where readers are already looking.

Pass total-items with per-page when the API reports a row count. Use total-pages when it reports only a page count. A derived count wins when both are present.

A range line beside the pager ("6–10 of 20"): the pager takes the whole row and fits its buttons to it, so a plain <p> beside it gets squeezed to one word per line. Put the line in a flex items-center gap-2 row and give it shrink-0 whitespace-nowrap. Skip justify-between and flex-wrap.

Always set label to name the list. The localized “Pagination” fallback is too generic when a screen has more than one result set.

State ownership

v-model is the current page. A standalone pager clamps it to 1..totalPages; page 0 becomes 1 and page 99 becomes the last page. With one page, every control is disabled, so you can omit the pager.

Model: 1

Listen to update:modelValue, never button clicks. Button availability and the ellipsis window are internal.

Every control is a button, or a link under navigation. The active page carries aria-current="page" and Previous/Next include localized accessible text.

Coming from v2loading prop

In v3 there is no loading prop. It was removed, not renamed. A leftover :loading="fetching" compiles and has no effect; a development build warns that it was removed. Use disabled and keep the loading affordance on the list or table.

Long ranges and narrow containers

Long ranges fold around the current page with ellipses, and you never compute the window yourself.

Page 1 of 42. The window folds around the current page — walk to the middle of the range and both ellipses appear.

max-size (default 6) caps how many page buttons show at once. The count excludes the ellipses and Previous / Next, and carries a ±1 tolerance so odd and even windows stay balanced around the current page.

It is a maximum, not a promise. When the container is too narrow, the control drops page buttons, never below three, and adds them back when the space returns. That is why the demo above changes width.

align places the control inside its full-width row (right by default, center, left), and ellipsis swaps the ... placeholder text. Neither the window nor the ellipses should be rebuilt by hand: hiding buttons with CSS breaks the fit logic that is already running.

Paired with a table

When the list is a BbTable, skip the wiring: give the table an id and the pagination the matching table-id.

Reference
Status
Total
ORD-2026-0417
Delivered
€1,298.00
ORD-2026-0418
Delivered
€338.80
ORD-2026-0421
Refunded
€899.00
ORD-2026-0426
Delivered
€477.00
ORD-2026-0430
Delivered
€2,196.00
5 per page

1–5 of 20

Both components publish their pagination props into the shared table context, so the page count is derived automatically and the current page stays two-way synced. There is no v-model here, mount order does not matter, the server render already shows the right page count, and any other component can join the same rendezvous with useBbTableContext('orders-pager') — a toolbar that resets the page after a filter change, a footer that shows the range.

The props are symmetric: page, per-page, total-items and total-pages are accepted by both sides, so the server's fields can go wherever is convenient. Pass each fact to one place, though — giving the table and the pager different total-items is an author error the library resolves arbitrarily. One rule settles conflicts: a count derived from a real total-items always wins, and total-pages is only a seed for when no row count is known.

Paired with a table the control does not clamp. The context owns the page, so an out-of-range value is the consumer's to fix — correcting it here would push a page nobody asked for. And mind the id: a typo does not throw, it pairs with nothing, and a development build warns the first time someone pages with no table mounted for that id.

Pages that survive a reload

Set navigation and the current page is reflected in the URL, so it is bookmarkable and survives a reload. query-key names the parameter, page by default. Under Vue Router or Nuxt every enabled button becomes a to link that merges the parameter into the current route's query, leaving other parameters alone; without a router it falls back to real hrefs built from window.location, so the same markup deep-links in a plain SPA. Disabled buttons never carry a link, so crawlers and middle-clicks cannot reach invalid pages.

vue
<BbPagination
    v-model="page"
    label="Exports pagination"
    navigation
    replace
    :total-pages="totalPages"
/>

Pair navigation with replace. Paging is a view control, not a journey: without it every click pushes a history entry, and someone who walked to page 12 needs eleven presses of Back to escape the list. With it the URL stays current — still deep-linkable, still reload-proof — and one Back returns to wherever they came from.

One thing stays yours: the component writes the URL, it never reads it. Seed the model from the route once, at setup, or a shared link lands on page 1:

ts
import { ref } from 'vue';
import { useRoute } from 'vue-router'; // or '#imports' in Nuxt

const route = useRoute();
const page = ref(Number(route.query.page) || 1);
Coming from v2querykey → queryKey

The prop was querykey, all lowercase, and it is now queryKey. The compiler cannot see it: a leftover querykey is ignored, URL sync moves back to ?page=, and every deep link and server route reading the old parameter stops matching. A development build warns, npx bitboss-ui check --fix renames it, and rg -i querykey finds the links and routes too.

Custom button content

Four slots repaint the buttons without touching their behaviour: item receives { item, active, disabled } for each page number, previous and next replace the chevrons, and ellipsis replaces the fold marker.

Q3 board pack · sheet 1 of 18

Cover

previous and next come with an accessibility bill. The chevrons ship with visually hidden, localized text behind them; replacing the slot content replaces that text too. Put a word in the button, or an sr-only span — but do not leave two buttons whose only content is an icon.

The ellipsis is not a button: it renders as a plain aria-hidden span, so it never lands in the accessibility tree as focusless noise. The page buttons stay real buttons with all their wiring; the slots only change what is painted inside them.