Paginator
A page navigation component for splitting content across multiple pages.
Usage
The controls are authored - how many pages to show, and where to elide a long range, is the page's decision
- and they are root-buttons, so a page control reads as the button it is. The paginator owns
the state: it moves aria-current="page" and sets
data-variant/data-color
on every control, and marks a step aria-disabled when there is nowhere left to go.
<nav is="root-paginator" aria-label="Paginator" current="2" total="3">
<button is="root-button" type="button" previous square aria-label="Previous page">
<i is="root-icon" icon="caret-left"></i>
</button>
<button is="root-button" type="button" data-page="1">1</button>
<button is="root-button" type="button" data-page="2">2</button>
<button is="root-button" type="button" data-page="3">3</button>
<button is="root-button" type="button" next square aria-label="Next page">
<i is="root-icon" icon="caret-right"></i>
</button>
</nav>
Clicking a page moves there; previous and next step one page and stop at the
ends.
data-page
names the page a control stands for - a control marked page instead takes the number in its
label, or its position in the row.
The steps carry a chevron - caret-left / caret-right, the same shape Select and
TreeView point with. A step drawn as a glyph alone needs aria-label: the icon is decorative,
so without one the control has no accessible name at all. A step that also says the word keeps both, and
needs no label.
The four shapes
Two independent choices - numbered pages or not, and what sits beside the steps - which is four rows in practice. The variant and color of every control follow from the first of them and are written by the component; see below.
Chevron steps and numbered pages. The densest of the four: the row is the page numbers, and the steps are two glyphs beside them.
<nav is="root-paginator" aria-label="Paginator" current="2" total="3">
<button is="root-button" type="button" previous square aria-label="Previous page">
<i is="root-icon" icon="caret-left"></i>
</button>
<button is="root-button" type="button" data-page="1">1</button>
<button is="root-button" type="button" data-page="2">2</button>
<button is="root-button" type="button" data-page="3">3</button>
<button is="root-button" type="button" next square aria-label="Next page">
<i is="root-icon" icon="caret-right"></i>
</button>
</nav>
Worded steps and a counter between them. No numbers to click, so the steps say what they do and the counter says where you are - which is the whole of the state in a row of three things.
<nav is="root-paginator" aria-label="Paginator" current="2" total="5">
<button is="root-button" type="button" previous>
<i is="root-icon" icon="caret-left"></i>Previous
</button>
<span data-part="counter" aria-live="polite">Page 2 of 5</span>
<button is="root-button" type="button" next>
Next<i is="root-icon" icon="caret-right"></i>
</button>
</nav>
A page-size choice, chevron steps and numbered pages. The select leads the row and the controls stay together at the end of it.
<nav is="root-paginator" aria-label="Paginator" current="2" total="5">
<select is="root-select" data-part="per-page" aria-label="Items per page">
<option value="10">10 / page</option>
<option value="20" selected>20 / page</option>
<option value="50">50 / page</option>
<option value="100">100 / page</option>
</select>
<button is="root-button" type="button" previous square aria-label="Previous page">
<i is="root-icon" icon="caret-left"></i>
</button>
<button is="root-button" type="button" data-page="1">1</button>
<button is="root-button" type="button" data-page="2">2</button>
<button is="root-button" type="button" data-page="3">3</button>
<button is="root-button" type="button" next square aria-label="Next page">
<i is="root-icon" icon="caret-right"></i>
</button>
</nav>
A page-size choice, worded steps and a counter. The fullest of the four, and the one a long table wants: how much is shown, where you are in it, and one step either way.
<nav is="root-paginator" aria-label="Paginator" current="2" total="5">
<select is="root-select" data-part="per-page" aria-label="Items per page">
<option value="10">10 / page</option>
<option value="20" selected>20 / page</option>
<option value="50">50 / page</option>
<option value="100">100 / page</option>
</select>
<button is="root-button" type="button" previous>
<i is="root-icon" icon="caret-left"></i>Previous
</button>
<button is="root-button" type="button" next>
Next<i is="root-icon" icon="caret-right"></i>
</button>
<span data-part="counter" aria-live="polite">Page 2 of 5</span>
</nav>
Variant and color
A page control does not name its own data-variant/data-color: the paginator
writes both, and which pair depends on what the row contains.
Numbered
- at least one [data-page]/[page]
control - every control, steps included, is quiet: tertiary/neutral. The page
you are on is the one exception, filled with primary/default, which is what
makes it read as the current page in a row that otherwise says nothing about itself.
Step-only
- no page controls, just the two steps - has nothing else in the row to carry emphasis, so the steps
take it themselves: secondary/default.
Items per page
An optional <select is="root-select" data-part="per-page">,
authored like the page controls: the paginator does not decide which sizes to offer, only reads which one
is picked. A change dispatches per-page-change and nothing else - the host re-renders its own
rows, which is what produces the new total and the new set of [data-page]
controls.
It is a root-select rather than a bare <select> for the reason the page
controls are
root-buttons: a native picker beside a row of root controls is the one thing in the
composition the design system is not painting. The field's 20ch floor is dropped here
- that is the width a form field wants, and roughly three times what "20 / page" needs next to a row of digits.
Placed by where it sits in the markup, not by an attribute: written before the page controls it sits on
the left of the row, written after them it sits on the right. An
auto
margin on the select is what does it, so the controls themselves stay together in the middle instead of
being spread across the row.
Page counter
An optional [data-part="counter"]: the paginator keeps its text current -
Page { current} of { lastPage} - on every navigation, so it is the one part of this
composition that is not purely authored. Meaningful with or without the select beside it; the two are
independent, and where each lands is again DOM order.
Sizes
data-size
takes small and large; medium is the default and needs no value written. The
step moves the row's gap, the floor a page control is drawn at, and the text with them - so a paginator
under a dense table can be smaller than one closing a page.
<nav is="root-paginator" aria-label="Small" data-size="small" current="2" total="3">
<button is="root-button" type="button" previous square aria-label="Previous page">
<i is="root-icon" icon="caret-left"></i>
</button>
<button is="root-button" type="button" data-page="1">1</button>
<button is="root-button" type="button" data-page="2">2</button>
<button is="root-button" type="button" data-page="3">3</button>
<button is="root-button" type="button" next square aria-label="Next page">
<i is="root-icon" icon="caret-right"></i>
</button>
</nav>
<nav is="root-paginator" aria-label="Large" data-size="large" current="2" total="3">
<button is="root-button" type="button" previous square aria-label="Previous page">
<i is="root-icon" icon="caret-left"></i>
</button>
<button is="root-button" type="button" data-page="1">1</button>
<button is="root-button" type="button" data-page="2">2</button>
<button is="root-button" type="button" data-page="3">3</button>
<button is="root-button" type="button" next square aria-label="Next page">
<i is="root-icon" icon="caret-right"></i>
</button>
</nav>
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
current |
number |
1 |
The page being shown; written by the component as it navigates |
total |
number |
last control | The last page there is, which is what bounds next |
data-size |
'small' | 'large' |
medium | Row gap, control floor and text step |
aria-label |
string |
'Paginator' |
Accessible label |
Parts
| Part | Element | Description |
|---|---|---|
per-page |
<select is="root-select"> |
Optional. The page-size choice; authored, read-only to the component |
counter |
any | Optional. "Page X of Y", kept in sync by the component |
Events
| Event | Detail | Description |
|---|---|---|
root-paginator.page-change |
{ page } |
Fired after the paginator moves to a new page. A click that does not move - a page that is already current, a step at the end of the range - emits nothing |
root-paginator.per-page-change |
{ perPage } |
Fired when [data-part="per-page"] changes. The paginator does not act on it
- re-rendering the rows, the total and the [data-page] controls is the
host's
|
Extends
HTMLElement
- use with <nav is="root-paginator">. Read currentPage for the
page being shown and lastPage for the end of the range; goTo(page) navigates,
and is what the controls call.