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.
import { PlPagination } from 'plass-ui';
<PlPagination count={12} page={page} onPageChange={setPage} />;import 'package:plass_ui/plass_ui.dart';
PlPagination(
count: 12,
page: page,
onPageChanged: (int next) => setState(() => page = next),
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| 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 |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| count * | number | — | How many pages there are. Fewer than two and the whole control renders nothing |
| page | number | — | The current page, 1-based. Use with onPageChange for a controlled row |
| defaultPage | number | 1 | Which page starts current, for an uncontrolled row |
| onPageChange | (page: number) => void | — | Called with the new page |
| siblingCount | number | 1 | How many pages are always shown on either side of the current one |
| boundaryCount | number | 1 | How many pages are always shown at each end. 0 drops the first and last, leaving only the window |
| showEdges | boolean | false | Shows the jump-to-first and jump-to-last steppers |
| showArrows | boolean | true | Shows the previous and next steppers |
| disabled | boolean | false | Every button in the row stops answering |
| getPageHref | (page: number) => string | — | The address of a page. With it every number becomes a real <a href> that a crawler can follow |
| renderLink | (page: number, href: string) => ReactElement | — | Renders 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 |
| label | string | 'Pagination' | Accessible name of the <nav> |
| pageLabel | (page: number) => string | `Page {n}` | Accessible name of a page button |
| previousLabel · nextLabel · firstLabel · lastLabel | string | — | Accessible 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 |
| Prop | Type | Default | Description |
|---|---|---|---|
| count * | int | — | How many pages there are. Fewer than two and the whole control renders nothing |
| page * | int | — | The current page, 1-based. Use with onPageChange for a controlled row |
| onPageChanged | ValueChanged<int>? | — | Called with the new page |
| variantshared | PlassVariant | PlassVariant.ghost | The material of a page at rest. The current page is always solid |
| sizeshared | PlassSize | PlassSize.md | Button height and type scale — the same ladder as PlButton, so a row lines up beside one |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.compact | Padding only — never the height, never the type scale |
| elevationshared | int | 0 | Drop shadow depth. 0 means no shadow at all |
| siblingCount | int | 1 | How many pages are always shown on either side of the current one |
| boundaryCount | int | 1 | How many pages are always shown at each end. 0 drops the first and last, leaving only the window |
| showEdges | bool | false | Shows the jump-to-first and jump-to-last steppers |
| showArrows | bool | true | Shows the previous and next steppers |
| disabled | bool | false | Every button in the row stops answering |
| label | String | 'Pagination' | Accessible name of the <nav> |
| pageLabel | String Function(int) | (page) => 'Page $page' | Accessible name of a page button |
| previousLabel · nextLabel · firstLabel · lastLabel | String | — | The 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.
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.
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.
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.
<PlPagination
count={12}
page={page}
getPageHref={(to) => `/articles?page=${to}`}
renderLink={(to, href) => <Link href={href} />}
/>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.
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 hiddenaria-liveline 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
labelis 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
pageLabelalready 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
| React | Flutter | Why |
|---|---|---|
getPageHref, renderLink | — | Flutter has no link element and nothing crawls a Flutter app, so the row is buttons and onPageChanged is where a router is called. |
defaultPage / onPageChange | page / onPageChanged | Flutter's own controls are controlled, and its name for the callback. |
statusLabel, as a live region | — | The 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 group | There 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 name | Flutter's semantics tree has no current. |
- Fewer than two pages renders nothing at all. A row with a lone disabled
1in 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.