Pagination
The custom element is <hui-pagination>.
Overview
Renders real <a href> links, so a host drives navigation with hx-get and the control still works without JavaScript. It owns only the page window, never the rows or counts.
Example
Source
<hui-pagination page="3" total="10" sibling-count="1" href="/items?page={page}" label="Pages"></hui-pagination>The Go template that renders it:
<hui-pagination page="{{ .Page }}" total="{{ .Total }}" sibling-count="{{ .Siblings }}" href="{{ .Href }}" label="{{ .Label }}"></hui-pagination>On a phone
When the full row does not fit, pagination steps down one form at a time until it does: Previous and Next become arrows (still named for assistive technology), then the window narrows to the current page between the first and last, then the numbers give way to “Page 6 of 20”. The form is measured, never written by the host, and reflected as data-compact.
Source
<div style="display:grid;gap:1rem;justify-items:start"> <hui-pagination page="6" total="20" sibling-count="2" href="/items?page={page}"></hui-pagination> <div style="width:240px"><hui-pagination page="6" total="20" href="/items?page={page}"></hui-pagination></div> <div style="width:180px"><hui-pagination page="6" total="20" href="/items?page={page}"></hui-pagination></div></div>API
Attributes6
| Name | Type | Default | Description |
|---|---|---|---|
page | number | 1 | The current page, clamped between 1 and total. Reflects to the page attribute. |
total | number | 1 | The number of pages. Reflects to the total attribute. |
sibling-count | number | 1 | How many pages to show either side of the current one before collapsing to an ellipsis. |
href | string | "" | Link template; {page} is replaced with each page number. Empty falls back to #page-N. |
label | string | "Pagination" | Accessible name for the <nav>. |
data-compact | 0 | 1 | 2 | 3 | 0 | Set by the element, never the host: which narrower form it is showing to fit its container. 1 draws Previous and Next as arrows, 2 also drops the siblings, 3 shows “Page N of M” in place of the numbers. |
Properties5
| Name | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Reflects to the page attribute. |
total | number | 1 | Reflects to the total attribute. |
sibling-count | number | 1 | Reflects to the sibling-count attribute. |
href | string | '' | Reflects to the href attribute. |
label | string | 'Pagination' | Reflects to the label attribute. |
CSS custom properties5
| Name | Default | Description |
|---|---|---|
--hui-accent | oklch(0.96 0.003 325.6) | Background of a hovered link. |
--hui-input | oklch(0.62 0.019 323.02) | Border of the current-page link. |
--hui-muted-foreground | oklch(0.542 0.034 322.5) | Colour of the ellipsis. |
--hui-radius-sm | calc(var(--hui-radius, 0.45rem) * 0.6) | Corner radius of a link. |
--hui-text-sm | 0.875rem | Font size of the links. |
::part() hooks1
| Name | Description |
|---|---|
ellipsis | The aria-hidden "…" that stands in for a collapsed run of pages. |
States
Single page
A single page: the numbers collapse to one link, and Previous and Next are both aria-disabled.
Source
<hui-pagination page="1" total="1" href="/items?page={page}"></hui-pagination>Wider window
sibling-count="2" shows two pages either side of the current one. When that does not fit its container the element steps down to a narrower form, which the phone example above shows.
Source
<hui-pagination page="6" total="20" sibling-count="2" href="/items?page={page}"></hui-pagination>On the first page
On the first page, Previous is aria-disabled and the window starts at page 1.
Source
<hui-pagination page="1" total="10" sibling-count="1" href="/items?page={page}"></hui-pagination>Server-side mechanics
The server owns the rows and the total count; it renders <hui-pagination> with only page, total, an href template and a label. The href is expanded with {page}, so real anchors point at server URLs and the control works without JavaScript, while the component owns only which page numbers appear. It is not form-associated, so it has no name and submits nothing. An hx-swap that replaces the pagination after a page change is the normal path; replacing it mid-click is harmless because each link is an ordinary navigation.
Accessibility
- The control is a
<nav>named bylabel; the current page carriesaria-current="page". - Every link has an accessible name: "Previous page", "Page N" or "Next page".
- Previous and Next carry
aria-disabledat the ends, but remain links so the no-JavaScript path is unchanged.
Keyboard
| Keys | Action |
|---|---|
Tab | Moves focus through the real links in order. |
Enter | Follows the focused link, exactly as a normal anchor does. |
Gotchas
- Previous and Next carry
aria-disabled="true"at the ends but remain real links, so they are still focusable and followable; the host must guard the action itself. - With an empty
hrefthe links fall back to#page-Nanchors, which navigate nowhere.