Pagination
Controlled page navigation for result lists, cards and tables. Supply the visible numbered pages and previous/next availability; Looma owns the native controls, current-page semantics and navigate requests. Optional hrefs preserve ordinary link navigation. Applications own routes, data fetching and page windows.
Numbered pages
The consumer supplies a bounded page window. Omitted page numbers produce a quiet ellipsis; the current page remains consumer controlled.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-pagination/ui-pagination.html"
>
<ui-pagination
page="3"
has-previous
has-next
pages="[{"value":1},{"value":2},{"value":3},{"value":4},{"value":10}]"
></ui-pagination><script setup>
import { UiPagination } from "@threadlabs/looma";
</script>
<template>
<UiPagination
:page="3"
:has-previous="true"
:has-next="true"
:pages="[{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 10 }]"
/>
</template>import { UiPagination } from "@threadlabs/looma";
export default function Example() {
return <UiPagination
page={3}
hasPrevious
hasNext
pages={[{"value":1},{"value":2},{"value":3},{"value":4},{"value":10}]}
/>;
}/** @jsxImportSource preact */
import { UiPagination } from "@threadlabs/looma";
export default function Example() {
return <UiPagination
page={3}
hasPrevious
hasNext
pages={[{"value":1},{"value":2},{"value":3},{"value":4},{"value":10}]}
/>;
}<script>
import { UiPagination } from "@threadlabs/looma";
</script>
<UiPagination
page={3}
hasPrevious
hasNext
pages={[{"value":1},{"value":2},{"value":3},{"value":4},{"value":10}]}
/>import { Component } from "@angular/core";
import { UiPagination } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiPagination],
template: `
<nav
ui-pagination
[page]="3"
[hasPrevious]="true"
[hasNext]="true"
[pages]="[{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 10 }]"
></nav>
`,
})
export default class Example {}Native links
Supplied hrefs stay ordinary links before upgrade and retain modified-click and open-in-new-tab behavior. Replace these example query destinations with your own routes.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-pagination/ui-pagination.html"
>
<ui-pagination
page="2"
label="Catalog pages"
has-previous
has-next
previous-href="?page=1"
next-href="?page=3"
pages="[{"value":1,"href":"?page=1"},{"value":2,"href":"?page=2"},{"value":3,"href":"?page=3"}]"
></ui-pagination><script setup>
import { UiPagination } from "@threadlabs/looma";
</script>
<template>
<UiPagination
:page="2"
label="Catalog pages"
:has-previous="true"
:has-next="true"
previous-href="?page=1"
next-href="?page=3"
:pages="[{ value: 1, href: '?page=1' }, { value: 2, href: '?page=2' }, { value: 3, href: '?page=3' }]"
/>
</template>import { UiPagination } from "@threadlabs/looma";
export default function Example() {
return <UiPagination
page={2}
label="Catalog pages"
hasPrevious
hasNext
previousHref="?page=1"
nextHref="?page=3"
pages={[{"value":1,"href":"?page=1"},{"value":2,"href":"?page=2"},{"value":3,"href":"?page=3"}]}
/>;
}/** @jsxImportSource preact */
import { UiPagination } from "@threadlabs/looma";
export default function Example() {
return <UiPagination
page={2}
label="Catalog pages"
hasPrevious
hasNext
previousHref="?page=1"
nextHref="?page=3"
pages={[{"value":1,"href":"?page=1"},{"value":2,"href":"?page=2"},{"value":3,"href":"?page=3"}]}
/>;
}<script>
import { UiPagination } from "@threadlabs/looma";
</script>
<UiPagination
page={2}
label="Catalog pages"
hasPrevious
hasNext
previousHref="?page=1"
nextHref="?page=3"
pages={[{"value":1,"href":"?page=1"},{"value":2,"href":"?page=2"},{"value":3,"href":"?page=3"}]}
/>import { Component } from "@angular/core";
import { UiPagination } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiPagination],
template: `
<nav
ui-pagination
[page]="2"
[label]="'Catalog pages'"
[hasPrevious]="true"
[hasNext]="true"
[previousHref]="'?page=1'"
[nextHref]="'?page=3'"
[pages]="[{ value: 1, href: '?page=1' }, { value: 2, href: '?page=2' }, { value: 3, href: '?page=3' }]"
></nav>
`,
})
export default class Example {}Unknown total
Previous and Next work without an invented page count. The application decides availability from its result or cursor data.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-pagination/ui-pagination.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-text/ui-text.html"
>
<ui-pagination
page="3"
has-previous
has-next
>
<ui-text
size="sm"
tone="secondary"
>Page 3</ui-text>
</ui-pagination><script setup>
import { UiPagination, UiText } from "@threadlabs/looma";
</script>
<template>
<UiPagination
:page="3"
:has-previous="true"
:has-next="true"
>
<UiText
size="sm"
tone="secondary"
>Page 3</UiText>
</UiPagination>
</template>import { UiPagination, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiPagination
page={3}
hasPrevious
hasNext
>
<UiText
size="sm"
tone="secondary"
>Page 3</UiText>
</UiPagination>;
}/** @jsxImportSource preact */
import { UiPagination, UiText } from "@threadlabs/looma";
export default function Example() {
return <UiPagination
page={3}
hasPrevious
hasNext
>
<UiText
size="sm"
tone="secondary"
>Page 3</UiText>
</UiPagination>;
}<script>
import { UiPagination, UiText } from "@threadlabs/looma";
</script>
<UiPagination
page={3}
hasPrevious
hasNext
>
<UiText
size="sm"
tone="secondary"
>Page 3</UiText>
</UiPagination>import { Component } from "@angular/core";
import { UiPagination, UiText } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiPagination, UiText],
template: `
<nav
ui-pagination
[page]="3"
[hasPrevious]="true"
[hasNext]="true"
>
<ng-template
ui-text
[size]="'sm'"
[tone]="'secondary'"
>Page 3</ng-template>
</nav>
`,
})
export default class Example {}Beside a results table
Table owns its rows, Pagination owns page controls, and Stack owns their spacing. Data retrieval and sorting stay with the application.
| Item | Status |
|---|---|
| Field guide | Available |
| Trail notes | Available |
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-table/ui-table.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-pagination/ui-pagination.html"
>
<ui-stack
gap="m"
>
<ui-table
density="compact"
>
<table>
<caption>Catalog results</caption>
<thead>
<tr>
<th
scope="col"
>Item</th>
<th
scope="col"
>Status</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>Field guide</th>
<td>Available</td>
</tr>
<tr>
<th
scope="row"
>Trail notes</th>
<td>Available</td>
</tr>
</tbody>
</table>
</ui-table>
<ui-pagination
page="1"
has-next
pages="[{"value":1},{"value":2},{"value":3}]"
></ui-pagination>
</ui-stack><script setup>
import { UiPagination, UiStack, UiTable } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="m"
>
<UiTable
density="compact"
>
<table>
<caption>Catalog results</caption>
<thead>
<tr>
<th
scope="col"
>Item</th>
<th
scope="col"
>Status</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>Field guide</th>
<td>Available</td>
</tr>
<tr>
<th
scope="row"
>Trail notes</th>
<td>Available</td>
</tr>
</tbody>
</table>
</UiTable>
<UiPagination
:page="1"
:has-next="true"
:pages="[{ value: 1 }, { value: 2 }, { value: 3 }]"
/>
</UiStack>
</template>import { UiPagination, UiStack, UiTable } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<UiTable
density="compact"
>
<table>
<caption>Catalog results</caption>
<thead>
<tr>
<th
scope="col"
>Item</th>
<th
scope="col"
>Status</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>Field guide</th>
<td>Available</td>
</tr>
<tr>
<th
scope="row"
>Trail notes</th>
<td>Available</td>
</tr>
</tbody>
</table>
</UiTable>
<UiPagination
page={1}
hasNext
pages={[{"value":1},{"value":2},{"value":3}]}
/>
</UiStack>;
}/** @jsxImportSource preact */
import { UiPagination, UiStack, UiTable } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="m"
>
<UiTable
density="compact"
>
<table>
<caption>Catalog results</caption>
<thead>
<tr>
<th
scope="col"
>Item</th>
<th
scope="col"
>Status</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>Field guide</th>
<td>Available</td>
</tr>
<tr>
<th
scope="row"
>Trail notes</th>
<td>Available</td>
</tr>
</tbody>
</table>
</UiTable>
<UiPagination
page={1}
hasNext
pages={[{"value":1},{"value":2},{"value":3}]}
/>
</UiStack>;
}<script>
import { UiPagination, UiStack, UiTable } from "@threadlabs/looma";
</script>
<UiStack
gap="m"
>
<UiTable
density="compact"
>
<table>
<caption>Catalog results</caption>
<thead>
<tr>
<th
scope="col"
>Item</th>
<th
scope="col"
>Status</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>Field guide</th>
<td>Available</td>
</tr>
<tr>
<th
scope="row"
>Trail notes</th>
<td>Available</td>
</tr>
</tbody>
</table>
</UiTable>
<UiPagination
page={1}
hasNext
pages={[{"value":1},{"value":2},{"value":3}]}
/>
</UiStack>import { Component } from "@angular/core";
import { UiPagination, UiStack, UiTable } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiTable, UiPagination],
template: `
<div
ui-stack
[gap]="'m'"
>
<div
ui-table
[density]="'compact'"
>
<table>
<caption>Catalog results</caption>
<thead>
<tr>
<th
scope="col"
>Item</th>
<th
scope="col"
>Status</th>
</tr>
</thead>
<tbody>
<tr>
<th
scope="row"
>Field guide</th>
<td>Available</td>
</tr>
<tr>
<th
scope="row"
>Trail notes</th>
<td>Available</td>
</tr>
</tbody>
</table>
</div>
<nav
ui-pagination
[page]="1"
[hasNext]="true"
[pages]="[{ value: 1 }, { value: 2 }, { value: 3 }]"
></nav>
</div>
`,
})
export default class Example {}API
Package: @threadlabs/looma · Native root: nav
Attributes
| Name | Description | Property | Type | Default | Allowed values |
|---|---|---|---|---|---|
disabled | Makes all page controls unavailable while keeping the current page visible. | disabled | boolean | false | — |
has-next | Whether another page is available. The component does not infer a total. | hasNext | boolean | false | — |
has-previous | Whether a preceding page is available. Previous is always unavailable on page one. | hasPrevious | boolean | false | — |
label | Accessible name for the navigation landmark. | label | string | Results pages | — |
next-href | Optional native destination for Next. Empty renders a request button. | nextHref | string | | — |
next-label | Accessible label for the following page action. | nextLabel | string | Next | — |
page | Current one-based page. The consumer updates it after a navigate request; activation never changes it internally. | page | number | 1 | — |
page-label | Prefix for each numbered page's accessible name, followed by its localized number. | pageLabel | string | Page | — |
pages | Visible numbered pages in ascending order. Omit for previous/next navigation without a known total. Gaps between numbers show an ellipsis. Supply href to keep a numbered page usable as a native link before JavaScript. | pages | list(object)
| [] | — |
previous-href | Optional native destination for Previous. Empty renders a request button. | previousHref | string | | — |
previous-label | Accessible label for the preceding page action. | previousLabel | string | Previous | — |
size | Uses Button's shared size scale. Narrow groups wrap without enlarging compact controls. | size | keyword | sm | sm, md, lg |
Framework props
| Name | Type | Default | Allowed values |
|---|---|---|---|
disabled | boolean | false | — |
hasNext | boolean | false | — |
hasPrevious | boolean | false | — |
label | string | Results pages | — |
nextHref | string | | — |
nextLabel | string | Next | — |
page | number | 1 | — |
pageLabel | string | Page | — |
pages | readonly ({ readonly value: number; readonly href?: string | null })[]
| [] | — |
previousHref | string | | — |
previousLabel | string | Previous | — |
size | "sm" | "md" | "lg" | sm | sm, md, lg |
Events
| Name | Detail | Description |
|---|---|---|
navigate | object
| Emitted with the declared detail payload. |
Slots
| Name | Purpose |
|---|---|
default | Default child content. |
Design tokens
Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.
Component tokens
No component tokens.
Shared tokens
No shared tokens.