Pagination

Page navigation for a long list.

Installation

Import the class, and @use the entry point in your global styles: without it the component renders unstyled.

import { Component } from '@angular/core';
import { WrPagination } from 'ngwr/pagination';

@Component({ imports: [WrPagination] })
export class MyComponent {}

Basic usage

Bind [total] and [(page)].

1 2 3 4 5 … 12 1 / 12
<wr-pagination [total]="120" [(page)]="page" />

Sizes

size: sm, md or lg.

1 2 3 4 5 … 12 1 / 12
1 2 3 4 5 … 12 1 / 12
1 2 3 4 5 … 12 1 / 12
<wr-pagination [total]="120" [(page)]="page" size="sm" />
<wr-pagination [total]="120" [(page)]="page" size="md" />
<wr-pagination [total]="120" [(page)]="page" size="lg" />

Shape

square flattens the corners, for example inside a data table.

1 2 3 4 5 … 12 1 / 12
1 2 3 4 5 … 12 1 / 12
<wr-pagination [total]="120" [(page)]="page" shape="rounded" />
<wr-pagination [total]="120" [(page)]="page" shape="square" />

With total + size changer

showTotal adds the range, showSizeChanger a page-size select, and align="end" moves it right.

1–10 of 320
1 2 3 4 5 … 32 1 / 32
<wr-pagination
  [total]="320"
  [(page)]="page"
  [(pageSize)]="size"
  showTotal
  showSizeChanger
  align="end"
/>

Narrow container (container query)

With responsive, the strip turns into a compact pager when its own box is narrow.

1 2 3 4 5 … 12 1 / 12
<div style="width: 260px"><wr-pagination responsive [total]="120" [(page)]="page" /></div>

Server-side paging

While total is 0, as during a request, the pager keeps the page you set and emits nothing for it.

page 1 — total 51
1 2 3 4 5 6 1 / 6
<wr-pagination
  [total]="total()"
  [page]="page()"
  (pageChange)="page.set($event)"
/>
private readonly http = inject(HttpClient);

readonly page = signal(1);
readonly size = signal(10);

// The resource drops its value while a new page loads, so total reads 0.
private readonly result = rxResource({
  params: () => ({ page: this.page(), size: this.size() }),
  stream: ({ params }) => this.http.get<{ data: User[]; total: number }>('/api/users', { params }),
});

readonly rows = computed(() => this.result.value()?.data ?? []);
readonly total = computed(() => this.result.value()?.total ?? 0);

Page size is the host's decision

Choosing a size only emits pageSizeChange. The page stays as it is: reset it yourself if you want.

<wr-pagination
  showSizeChanger
  [total]="total()"
  [page]="page()"
  (pageChange)="page.set($event)"
  [pageSize]="size()"
  (pageSizeChange)="onSizeChange($event)"
/>
onSizeChange(next: number): void {
  this.size.set(next);
  this.page.set(1);
}

Keeping the last good total

To keep the strip steady during a request, hold the last total in a linkedSignal.

readonly total = linkedSignal<number | undefined, number>({
  source: () => this.result.value()?.total,
  computation: (next, previous) => next ?? previous?.value ?? 0,
});

Keyboard, roles and accessible names

Every cell is a tab stop; Enter or Space activates it.

  • label, prevLabel, nextLabel and itemsPerPageLabel override the text from the i18n catalog.
  • In end-to-end tests, use getByRole('button', { name: 'Go to page 2' }); the current page has aria-current="page".
  • In unit tests, use WrPaginationHarness from ngwr/pagination/testing.

API

NameDescriptionTypeDefault
pageCurrently displayed page (1-based). Two-way bindable. page rather than currentPage since v14, so this and <wr-table> — which renders one of these in its own footer — spell the two concepts they share one way each. It also names the output: a model() called X forces (XChange), so the old name produced (currentPageChange) against the table's (pageChange).number1
pageSizeItems per page. Two-way bindable.number10
totalTotal item count across all pages.number0
pageSizeOptionsOptions shown in the page-size dropdown.readonly number[][10, 20, 50, 100]
showSizeChangerRender the page-size dropdown.booleanfalse
showTotalRender the "X–Y of Z" total label.booleanfalse
alignHorizontal alignment.WrPaginationAlign'start'
sizeSize variant — cascades to every internal button.WrPaginationSize'sm'
shapeCell corner treatment.WrPaginationShape'rounded'
disabledDisable interaction.booleanfalse
responsiveCollapse to a compact ‹ page / total › pager when the control's own box is too narrow for the full numbered strip (a container query on its own width, not the viewport).booleanfalse
prevLabelPrevious-page button aria-label. Falls back to pagination.prev.string | nullnull
nextLabelNext-page button aria-label. Falls back to pagination.next.string | nullnull
itemsPerPageLabel"Items per page" label. Falls back to pagination.itemsPerPage.string | nullnull
labelAccessible name for the role="navigation" host. Falls back to pagination.label.string | nullnull

CSS variables

Set these on the component's selector or a wrapper, not on :root: the defaults are declared on ngwr/pagination's own element.

VariableDefaultDeclared on
--wr-pagination-cap-radiusvar(--wr-border-radius-sm).wr-pagination
--wr-pagination-cell1.75rem only under .wr-pagination--sm — unset elsewhere.wr-pagination--sm +2 variant overrides
--wr-pagination-cell-padding-xvar(--wr-space-sm).wr-pagination
--wr-pagination-current-font-sizevar(--wr-text-sm).wr-pagination
--wr-pagination-current-padding-xvar(--wr-space-sm).wr-pagination
--wr-pagination-font-familyvar(--wr-font-family-base).wr-pagination
--wr-pagination-inner-gapvar(--wr-space-lg).wr-pagination
--wr-pagination-nav-gapvar(--wr-space-xs).wr-pagination
--wr-pagination-padding-blockvar(--wr-space-sm).wr-pagination
--wr-pagination-total-font-sizevar(--wr-text-sm).wr-pagination

Dependencies

Built onButton, Select
Used byTable

See also