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="[{&quot;value&quot;:1},{&quot;value&quot;:2},{&quot;value&quot;:3},{&quot;value&quot;:4},{&quot;value&quot;: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="[{&quot;value&quot;:1,&quot;href&quot;:&quot;?page=1&quot;},{&quot;value&quot;:2,&quot;href&quot;:&quot;?page=2&quot;},{&quot;value&quot;:3,&quot;href&quot;:&quot;?page=3&quot;}]"
></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.

Catalog results
ItemStatus
Field guideAvailable
Trail notesAvailable

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="[{&quot;value&quot;:1},{&quot;value&quot;:2},{&quot;value&quot;: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

NameDescriptionPropertyTypeDefaultAllowed values
disabledMakes all page controls unavailable while keeping the current page visible.disabledbooleanfalse—
has-nextWhether another page is available. The component does not infer a total.hasNextbooleanfalse—
has-previousWhether a preceding page is available. Previous is always unavailable on page one.hasPreviousbooleanfalse—
labelAccessible name for the navigation landmark.labelstringResults pages—
next-hrefOptional native destination for Next. Empty renders a request button.nextHrefstring—
next-labelAccessible label for the following page action.nextLabelstringNext—
pageCurrent one-based page. The consumer updates it after a navigate request; activation never changes it internally.pagenumber1—
page-labelPrefix for each numbered page's accessible name, followed by its localized number.pageLabelstringPage—
pagesVisible 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.pageslist(object)
  • [].value: number (required)
  • [].href: string
[]—
previous-hrefOptional native destination for Previous. Empty renders a request button.previousHrefstring—
previous-labelAccessible label for the preceding page action.previousLabelstringPrevious—
sizeUses Button's shared size scale. Narrow groups wrap without enlarging compact controls.sizekeywordsmsm, md, lg

Framework props

NameTypeDefaultAllowed values
disabledbooleanfalse—
hasNextbooleanfalse—
hasPreviousbooleanfalse—
labelstringResults pages—
nextHrefstring—
nextLabelstringNext—
pagenumber1—
pageLabelstringPage—
pagesreadonly ({ readonly value: number; readonly href?: string | null })[]
  • [].value: number (required)
  • [].href: string
[]—
previousHrefstring—
previousLabelstringPrevious—
size"sm" | "md" | "lg"smsm, md, lg

Events

NameDetailDescription
navigateobject
  • page: number (required)
  • previousPage: number (required)
  • trigger: keyword (required)
Emitted with the declared detail payload.

Slots

NamePurpose
defaultDefault 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.