Skip to content

PlPagination

The strip of page numbers under a long list. Every button in it is a real PlButton, so it lines up with any other control of the same size.

React
tsx
import { PlPagination } from 'plass-ui';

<PlPagination count={12} page={page} onPageChange={setPage} />;
dart
import 'package:plass_ui/plass_ui.dart';

PlPagination(
  count: 12,
  page: page,
  onPageChanged: (int next) => setState(() => page = next),
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''ghost'The material of a page at rest. The current page is always solid
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Button height and type scale — the same ladder as PlButton, so a row lines up beside one
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''compact'Padding only — never the height, never the type scale
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
count * numberHow many pages there are. Fewer than two and the whole control renders nothing
pagenumberThe current page, 1-based. Use with onPageChange for a controlled row
defaultPagenumber1Which page starts current, for an uncontrolled row
onPageChange(page: number) => voidCalled with the new page
siblingCountnumber1How many pages are always shown on either side of the current one
boundaryCountnumber1How many pages are always shown at each end. 0 drops the first and last, leaving only the window
showEdgesbooleanfalseShows the jump-to-first and jump-to-last steppers
showArrowsbooleantrueShows the previous and next steppers
disabledbooleanfalseEvery button in the row stops answering
getPageHref(page: number) => stringThe address of a page. With it every number becomes a real <a href> that a crawler can follow
renderLink(page: number, href: string) => ReactElementRenders each page's link as something other than an <a> — a router's Link. Without it, every press in a single-page app is a full document load
labelstring'Pagination'Accessible name of the <nav>
pageLabel(page: number) => string`Page {n}`Accessible name of a page button
previousLabel · nextLabel · firstLabel · lastLabelstringAccessible names of the steppers. None of them is ever drawn
statusLabel(page: number, count: number) => string`Page {n} of {total}`The live-region sentence a screen reader hears when the page changes
PropTypeDefaultDescription
count * intHow many pages there are. Fewer than two and the whole control renders nothing
page * intThe current page, 1-based. Use with onPageChange for a controlled row
onPageChangedValueChanged<int>?Called with the new page
variantsharedPlassVariantPlassVariant.ghostThe material of a page at rest. The current page is always solid
sizesharedPlassSizePlassSize.mdButton height and type scale — the same ladder as PlButton, so a row lines up beside one
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.compactPadding only — never the height, never the type scale
elevationsharedint0Drop shadow depth. 0 means no shadow at all
siblingCountint1How many pages are always shown on either side of the current one
boundaryCountint1How many pages are always shown at each end. 0 drops the first and last, leaving only the window
showEdgesboolfalseShows the jump-to-first and jump-to-last steppers
showArrowsbooltrueShows the previous and next steppers
disabledboolfalseEvery button in the row stops answering
labelString'Pagination'Accessible name of the <nav>
pageLabelString Function(int)(page) => 'Page $page'Accessible name of a page button
previousLabel · nextLabel · firstLabel · lastLabelStringThe names of the steppers. Never drawn

Every native <nav> attribute passes straight through. color is excluded because it collides with the color in the table above, and onChange because the row spells it onPageChange.

The row is controlled: it is handed the current page and reports the one that was chosen.

What the shared axes (variant size color density elevation) mean across the library is in prop conventions.

Examples

variant

Sets how a page at rest looks. The current page is always solid, whatever the row's resting variant is. It is the one thing here that has to be legible without being read.

The default is ghost rather than the solid a lone PlButton takes: nine panes of tinted glass in a row say that all nine are the primary action.

React

siblingCount and boundaryCount

boundaryCount is how many pages stay pinned at each end; siblingCount is how many sit either side of the current page. Everything between is an ellipsis, except a gap of exactly one page, which is filled with that page instead, because 1 … 3 … 9 hides a single number behind a symbol wider than the number it replaced.

The row keeps a constant number of slots whatever page it is on: the window slides toward whichever end it is near rather than being clipped by it. Without that, stepping from page 1 to page 2 would relayout the row and move every button out from under the pointer that just pressed one.

React

showArrows and showEdges

The steppers are icon-only buttons, so they go square and land on exactly the same footprint as a single-digit page, a row whose ends are a different width from its middle reads as two controls pushed together. At either end of the range the relevant steppers are disabled and stay in place, so the row never shifts sideways.

React

getPageHref

Turns every number into a real <a href>. Without it the row is buttons, and a crawler cannot press one. A paged list of articles or products then exists for a reader and stops at page one for everything else.

With an href and an onPageChange, the handler wins and the navigation is cancelled: that is a client-side router keeping the page it already has. With an href and no handler, the link is left to do what a link does, which is also what makes the row work before JavaScript has loaded. A press carrying , Ctrl, Shift or Alt is never cancelled: that is the reader asking the browser for a new tab.

The current page and a stepper at the end of the row stay <button>s, because disabled is not something an <a> can be.

renderLink decides what that link is made of. A bare <a> is a full document load in a single-page app. The router never sees the press, so the whole page is fetched, parsed and booted again to change one number. Hand back the Link your router brings and the address arrives already built, so there is no second copy of getPageHref inside it. rel="prev" and rel="next" are merged onto whatever comes back.

tsx
<PlPagination
  count={12}
  page={page}
  getPageHref={(to) => `/articles?page=${to}`}
  renderLink={(to, href) => <Link href={href} />}
/>
React

size

The same height ladder as PlButton, so a pagination and a button on the same row keep their baseline. density defaults to compact here: a number needs less room beside it than a word.

React

Accessibility

  • Renders a <nav> around a <ul>: a named landmark a screen reader can skip, holding a list whose length says how far the pages go.
  • The current page carries aria-current="page", and a visually hidden aria-live line says which page of how many. The list length alone does not, once an ellipsis is in it.
  • Every button has an accessible name (Page 4, Next page). All of them are props, so a page in another language sets its own; nothing here is ever drawn.
  • The ellipsis is an aria-hidden <span>, not a disabled button. It is punctuation, not a control that happens to be unavailable.
  • The row is a named group, and label is that name.
  • Every button has a name of its own, "Page 4", "Next page". All of them are parameters, so a screen in another language sets its own; nothing here is ever drawn.
  • The digit on a page button is excluded from what is read, because pageLabel already says it, a label that merged both would announce the number twice.
  • The ellipsis is excluded from semantics entirely. It is punctuation, not a control that happens to be unavailable.
  • A stepper at the end of the range is disabled and stays in place, so the row never shifts sideways.

Differences from the React build

ReactFlutterWhy
getPageHref, renderLinkFlutter has no link element and nothing crawls a Flutter app, so the row is buttons and onPageChanged is where a router is called.
defaultPage / onPageChangepage / onPageChangedFlutter's own controls are controlled, and its name for the callback.
statusLabel, as a live regionThe current page is announced by its own button's name when focus reaches it, and a live region that fired on every page change would talk over the list it just replaced.
<nav> around a <ul>a named semantics groupThere is no landmark to skip to, and no list semantics for a reset to take away.
aria-current="page"the filled variant, and the button's nameFlutter's semantics tree has no current.
  • Fewer than two pages renders nothing at all. A row with a lone disabled 1 in it is a control advertising that it has nothing to do.
  • The steppers turn one chevron glyph rather than shipping four drawings, and they flip under RTL.

Released under the MIT License