Skip to content

BbPagination

A page selector for long lists — bind the current page, let it fold long ranges and fit its container, and drive your own data from what it reports.

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

Pass Through

Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.

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

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-0417Delivered1298.00 EUR
  • ORD-2026-0418Delivered338.80 EUR
  • ORD-2026-0421Refunded899.00 EUR
  • ORD-2026-0426Delivered477.00 EUR
  • ORD-2026-0430Delivered2196.00 EUR

20 orders

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.

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.

Page 1 of 12

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 v2

In v3 there is no loading prop. It was removed, not renamed. A leftover :loading="fetching" compiles but does nothing. 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: a ResizeObserver drops page buttons when the container is too narrow — never below three — and adds them back when the space returns. That is why the demo above is resizable, and why there is no "max visible items" prop to fight with.

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.

Orders
Reference
Status
Total
ORD-2026-0417
Delivered
1298.00 EUR
ORD-2026-0418
Delivered
338.80 EUR
ORD-2026-0421
Refunded
899.00 EUR
ORD-2026-0426
Delivered
477.00 EUR
ORD-2026-0430
Delivered
2196.00 EUR

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, 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 error, it just pairs with nothing.

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.

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 v2

The prop was querykey, all lowercase, and it is now queryKey. This one is silent — a leftover querykey is simply ignored, URL sync moves back to ?page=, and every deep link and every server route reading the old parameter breaks quietly. rg -i querykey finds them all, kebab spelling included. Disabled buttons never carry a link, so crawlers and middle-clicks cannot reach invalid pages.

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.

Sheet 1 of 18 · Q3 board pack

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 a glyph.

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.