Pagination
FreeSmart page-window with ellipsis, a page-size selector, 3 variants and 3 sizes.
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 demoMore examples
With page-size selector & summary
<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
| Name | Type | Default | Description |
|---|---|---|---|
| totalItems | number | null | 0 | Total 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`. |
| mode | SvPaginationMode | 'offset' | `'offset'` for `?page=`-style APIs (numbered pages, needs a total), `'cursor'` for keyset APIs (Previous/Next and an ordinal, needs neither). |
| nextCursor | string | null | null | `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. |
| hasMore | boolean | null | null | Override "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. |
| loading | boolean | false | True 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. |
| error | SvDataError | null | null | A failed page load, normalized — `docs/async-contract-adr.md`. Rendered as a `role="alert"` beside the controls. Unnecessary when `pageAction` is bound. |
| retryLabel | string | null | null | Label 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) | null | null | Hand 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. |
| pageSizeChangePolicy | SvPageSizePolicy | 'preserve-first-item' | Where the user lands when the page size changes. |
| pageSize | number (two-way) | 10 | Number of items per page. Use `[(pageSize)]` for two-way binding. |
| currentPage | number (two-way) | 1 | 1-based current page index. Use `[(currentPage)]` for two-way binding. |
| pageSizeOptions | number[] | [10, 25, 50, 100] | Options shown in the page-size `<select>`. Empty array hides the selector entirely. |
| urlKey | string | null | null | Put 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. |
| surfaceId | string | null | null | Names this paginator in the shared navigation-analytics stream, when a page has more than one. Ignored unless `provideSvNavTelemetry()` is wired up. |
| variant | PaginationVariant | 'default' | Visual variant. |
| size | PaginationSize | 'md' | Control size. |
| showSummary | boolean | true | Show total-item count summary (e.g. "Showing 1–10 of 234"). |
| ariaLabel | string | 'Pagination' | Accessible label for the navigation landmark. |
| customClass | string | '' | Additional CSS classes on the root wrapper. |
| scrollToTop | boolean | false | Scroll 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. |
| scrollTarget | HTMLElement | null | null | With `scrollToTop`, the element to bring into view — usually the top of the results list. `null` scrolls the paginator itself into view. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| pageChange | number | Emitted when the user navigates to a different page. |
| unresolved | SvUnresolvedValue<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. |
| cursorChange | string | 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]`. |
| retryRequested | void | The Try-again button was pressed and no `pageAction` is bound, so the host owns the retry. Only fires when `retryLabel` is set. |
| loadFailed | SvDataError | A `pageAction` rejected. Already normalized; also readable as `activeError()`. |
| pageSizeChange | number | Emits the new `pageSize` whenever it changes — the output half of `[(pageSize)]`. |
| currentPageChange | number | Emits 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.
<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.
// 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.