Pagination

Free

Smart page-window with ellipsis, a page-size selector, 3 variants and 3 sizes.

</>sv-pagination paginationnavigationpages
import { SvPaginationComponent } from '@styloviz/pagination';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

With page-size selector & summary

example.htmlAngular
<sv-pagination
  [totalItems]="240"
  [pageSize]="20"
  [pageSizeOptions]="[10, 20, 50]"
  [currentPage]="page"
  [showSummary]="true"
  (pageChange)="load($event)" />

When to use

  • Job: result-set position — renders buttons and emits `pageChange`; it owns no data and no URL (compare all seven in the “Choosing a navigation pattern” guide under Guides).
  • Splitting a large result set into navigable pages.
  • Tables and lists where loading everything at once is impractical.
  • For continuous feeds, infinite scroll (List View) can feel more fluid.

Async states

  • loading
  • error
  • partial

Inputs

NameTypeDefaultDescription
totalItemsnumber | null0Total number of items across all pages, or `null` when the backend cannot report one. `null` hides the numbered pages and the count summary — see `mode`.
modeSvPaginationMode'offset'`'offset'` for `?page=`-style APIs (numbered pages, needs a total), `'cursor'` for keyset APIs (Previous/Next and an ordinal, needs neither).
nextCursorstring | nullnull`mode="cursor"` only: the cursor the server returned for the page *after* the one on screen. `null` means this is the last page, so Next disables.
hasMoreboolean | nullnullOverride "is there a page after this one?" — for an offset API that reports no total but does know whether more rows exist (`LIMIT n + 1`). `null` lets the component infer it from the total, or from `nextCursor` in cursor mode.
loadingbooleanfalseTrue while the host is loading a page. Every control disables, the nav is `aria-busy`, and a second press is refused rather than queued. Bind this when the host owns the fetch; bind `pageAction` instead and the component tracks it for you.
errorSvDataError | nullnullA failed page load, normalized — `docs/async-contract-adr.md`. Rendered as a `role="alert"` beside the controls. Unnecessary when `pageAction` is bound.
retryLabelstring | nullnullLabel for the Try-again button. Non-null is what RENDERS it: an output cannot be asked whether anyone is listening, and a retry button that does nothing is worse than none.
pageAction((request: SvPageRequest) => unknown) | nullnullHand the component the page load itself. It then owns the sequencing: a second press while one is in flight is refused (not queued, not superseded), and a response that arrives after a cancel is discarded. Pass `request.signal` to your transport.
pageSizeChangePolicySvPageSizePolicy'preserve-first-item'Where the user lands when the page size changes.
pageSizenumber (two-way)10Number of items per page. Use `[(pageSize)]` for two-way binding.
currentPagenumber (two-way)11-based current page index. Use `[(currentPage)]` for two-way binding.
pageSizeOptionsnumber[][10, 25, 50, 100]Options shown in the page-size `<select>`. Empty array hides the selector entirely.
urlKeystring | nullnullPut the page number in the URL under this key, so a page can be shared, refreshed and reached with the back button. Needs `provideSvNavLocation()` at the app root; give a second paginator on the same page a different key.
surfaceIdstring | nullnullNames this paginator in the shared navigation-analytics stream, when a page has more than one. Ignored unless `provideSvNavTelemetry()` is wired up.
variantPaginationVariant'default'Visual variant.
sizePaginationSize'md'Control size.
showSummarybooleantrueShow total-item count summary (e.g. "Showing 1–10 of 234").
ariaLabelstring'Pagination'Accessible label for the navigation landmark.
customClassstring''Additional CSS classes on the root wrapper.
scrollToTopbooleanfalseScroll back to the top of the results after a page change. Leave it off when the paginator is inside a card or a scroll container of its own.
scrollTargetHTMLElement | nullnullWith `scrollToTop`, the element to bring into view — usually the top of the results list. `null` scrolls the paginator itself into view.

Outputs

EventPayload typeDescription
pageChangenumberEmitted when the user navigates to a different page.
unresolvedSvUnresolvedValue<number>Emitted when `currentPage` names a page that does not exist. Carries what was asked for, what is shown instead, why, and whether `currentPage` was corrected.
cursorChangestring | null`mode="cursor"` only: the cursor of the page now on screen (`null` for the first page). Fetch with this; hand the response's next cursor back through `[nextCursor]`.
retryRequestedvoidThe Try-again button was pressed and no `pageAction` is bound, so the host owns the retry. Only fires when `retryLabel` is set.
loadFailedSvDataErrorA `pageAction` rejected. Already normalized; also readable as `activeError()`.
pageSizeChangenumberEmits the new `pageSize` whenever it changes — the output half of `[(pageSize)]`.
currentPageChangenumberEmits the new `currentPage` whenever it changes — the output half of `[(currentPage)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Bound to the names this component actually exposes.

usage.htmlAngular
<sv-pagination
  [pageSizeOptions]="data.items()"
  [loading]="data.loading()"
  [error]="data.error()"
  [hasMore]="data.hasMore()"
/>

Host class

Drive the three async inputs from your own fetch.

host.component.tsTypeScript
// This component reports its state but does not emit a query, so drive
// it from your own fetch and bind the three async inputs.
readonly items = signal<Row[]>([]);
readonly loading = signal(false);
readonly error = signal<SvDataError | null>(null);

async load(): Promise<void> {
  this.loading.set(true);
  this.error.set(null);
  try {
    this.items.set(await this.api.fetchRows());
  } catch (e) {
    this.error.set(toSvDataError(e));
  } finally {
    this.loading.set(false);
  }
}
  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `pageSizeOptions`, `loading` and `error`.
  • `hasMore` and `loadMore` drive incremental paging; bind `hasMore` from the controller's own signal of the same name.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • Ellipsis page window.
  • Optional page-size selector.
  • Three variants and sizes.

Best practices

  • Do: Show a summary ("21–40 of 240") so users know where they are.
  • Do: Keep the page window compact with ellipsis for large totals.
  • Tip: Offer a `pageSizeOptions` selector for power users scanning lots of rows.

Accessibility

  • Controls are labelled buttons inside a labelled nav landmark (`ariaLabel`).
  • Current page exposed via aria-current.

Feedback on Pagination

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.