Skip to main content

Pagination

Navigate a result set by page using state or destinations.

Use Pagination when a result set has a known page count. The application owns data fetching and the mapping from page numbers to URLs or controlled state.

Basic usage#

import { Pagination } from 'moraine'

export function Example() {
  return <Pagination total={100} itemsPerPage={10} defaultPage={1} />
}

Playground#

Current Page: 1 of 10
Props
Slots

Anatomy#

Pagination [component; slot=root; <nav>]
├── list [slot]
│   ├── listItem [slot]
│   │   └── prev [slot]
│   ├── listItem [slot]
│   │   └── item [slot]
│   └── listItem [slot]
│       └── next [slot]
└── status [internal]

Usage#

Current page#

Use page with onPageChange for controlled navigation. total is the number of records, not the page count; the component derives the page count from itemsPerPage. The surrounding application owns data fetching and URL updates.

import { Pagination } from 'moraine'
import { createSignal } from 'solid-js'

export function PageState() {
  const [page, setPage] = createSignal(1)

  return (
    <div class="flex flex-col gap-3 items-center">
      <Pagination total={100} itemsPerPage={10} page={page()} onPageChange={setPage} />
      <p class="text-xs text-muted-foreground">
        Active page: <span class="text-foreground font-medium">{page()}</span> of 10
      </p>
    </div>
  )
}

Use to(page) by itself to render enabled page controls as anchors. For router integration, use itemAs for numbered items and controlAs for Previous/Next; these props are independent and to still only generates the destination. This example uses Solid Router’s A and useSearchParams to keep the current page synchronized with destinations such as /pagination?page=2. Boundary Previous/Next controls remain native disabled buttons even when controlAs is set.

import { A, useSearchParams } from '@solidjs/router'
import { Pagination } from 'moraine'
import { createMemo } from 'solid-js'

export function LinkRendering() {
  const [params] = useSearchParams()
  const page = createMemo(() => {
    const requested = Number(params.page)
    return Number.isFinite(requested) ? Math.min(5, Math.max(1, Math.trunc(requested))) : 1
  })
  return (
    <div class="flex w-full justify-center">
      <Pagination
        itemAs={A}
        controlAs={A}
        total={50}
        itemsPerPage={10}
        page={page()}
        siblingCount={1}
        to={(page) => `/pagination?page=${page}`}
      />
    </div>
  )
}

Keyboard interaction#

Pagination does not add collection-style arrow navigation. Each interactive page control uses the keyboard semantics of its rendered button, link, or custom polymorphic element.

Examples#

Controlled page#

import { Badge, Pagination } from 'moraine'
import { createMemo, createSignal, For } from 'solid-js'

export function Controlled() {
  const [page, setPage] = createSignal(1)
  const itemsPerPage = 3

  const ALL_CUSTOMERS = [
    { id: 1, name: 'Stripe Inc.', plan: 'Enterprise', status: 'Active', mrr: '$2,400' },
    { id: 2, name: 'Vercel Labs', plan: 'Enterprise', status: 'Active', mrr: '$1,800' },
    { id: 3, name: 'Linear App', plan: 'Pro', status: 'Active', mrr: '$450' },
    { id: 4, name: 'Supabase Inc.', plan: 'Enterprise', status: 'Active', mrr: '$3,200' },
    { id: 5, name: 'Resend Co.', plan: 'Pro', status: 'Active', mrr: '$600' },
    { id: 6, name: 'Raycast HQ', plan: 'Pro', status: 'Trial', mrr: '$0' },
    { id: 7, name: 'Figma Design', plan: 'Enterprise', status: 'Active', mrr: '$4,500' },
    { id: 8, name: 'Midjourney AI', plan: 'Enterprise', status: 'Active', mrr: '$6,000' },
  ]

  const currentRecords = createMemo(() => {
    const start = (page() - 1) * itemsPerPage
    return ALL_CUSTOMERS.slice(start, start + itemsPerPage)
  })

  return (
    <div class="p-4 b-1 b-border rounded-xl bg-card max-w-xl space-y-4">
      <div class="flex items-center justify-between">
        <h4 class="text-sm font-semibold">Active Customers</h4>
        <span class="text-xs text-muted-foreground">
          Showing {(page() - 1) * itemsPerPage + 1}–
          {Math.min(page() * itemsPerPage, ALL_CUSTOMERS.length)} of {ALL_CUSTOMERS.length} records
        </span>
      </div>

      <div class="text-xs divide-border divide-y">
        <For each={currentRecords()}>
          {(customer) => (
            <div class="py-2.5 flex items-center justify-between">
              <div>
                <p class="text-foreground font-medium">{customer.name}</p>
                <p class="text-muted-foreground">{customer.plan} tier</p>
              </div>
              <div class="flex gap-3 items-center">
                <Badge variant={customer.status === 'Active' ? 'solid' : 'outline'} size="sm">
                  {customer.status}
                </Badge>
                <span class="font-medium font-mono">{customer.mrr}/mo</span>
              </div>
            </div>
          )}
        </For>
      </div>

      <div class="pt-2 border-t border-border flex justify-center">
        <Pagination
          page={page()}
          onPageChange={setPage}
          total={ALL_CUSTOMERS.length}
          itemsPerPage={itemsPerPage}
          siblingCount={1}
          prevText="Previous"
          nextText="Next"
        />
      </div>
    </div>
  )
}

Page size and results#

Keep the requested page within the new page count when users change how many results are shown.

import { Field, Pagination, Select } from 'moraine'
import type { SelectT } from 'moraine'
import { createMemo, createSignal, For } from 'solid-js'

const RESULTS = [
  'Account settings',
  'Billing history',
  'Connected apps',
  'Developer keys',
  'Email preferences',
  'Export requests',
  'Member invitations',
  'Notification rules',
  'Privacy controls',
  'Team permissions',
  'Usage reports',
  'Workspace details',
]

const PAGE_SIZES: SelectT.Item<number>[] = [
  { value: 3, label: '3 results' },
  { value: 6, label: '6 results' },
  { value: 12, label: '12 results' },
]

export function PageSize() {
  const [page, setPage] = createSignal(1)
  const [pageSize, setPageSize] = createSignal(3)
  const visible = createMemo(() => {
    const start = (page() - 1) * pageSize()
    return RESULTS.slice(start, start + pageSize())
  })

  return (
    <div class="max-w-md w-full space-y-4">
      <Field label="Results per page">
        <Select
          items={PAGE_SIZES}
          value={pageSize()}
          onValueChange={(value) => {
            if (value === null) {
              return
            }
            setPageSize(value)
            setPage(1)
          }}
        />
      </Field>
      <ul class="text-sm border-y border-border divide-border divide-y">
        <For each={visible()}>{(result) => <li class="py-2">{result}</li>}</For>
      </ul>
      <Pagination
        total={RESULTS.length}
        itemsPerPage={pageSize()}
        page={page()}
        onPageChange={setPage}
      />
    </div>
  )
}

Attributes#

Attributes
data-disabledSlot: pagination-item, pagination-prev, pagination-nextDescription: Present when the component, slot, or item is disabled.
data-loadingSlot: pagination-item, pagination-prev, pagination-nextDescription: Present when the component or async operation is loading.
data-textSlot: pagination-prev, pagination-nextDescription: Present when the input group part contains text.
data-currentSlot: pagination-itemDescription: Present when the item represents the current location or step.
data-ellipsisSlot: pagination-ellipsisDescription: —

Props#

Props for the Pagination component. Renders a <nav> element by default.

Prop