> ## Documentation Index
> Fetch the complete documentation index at: https://vlyai-1c28d863.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Infinite scroll vs. paginated tables. Cursors vs. offsets. Convex pagination patterns.

Two pagination styles in most apps:

<CardGroup cols={2}>
  <Card title="Infinite scroll" icon="scroll-text">
    For feeds — chat, social, activity. User scrolls; new items load. Best for chronological browsing.
  </Card>

  <Card title="Paginated table" icon="table">
    For data tables — admin panels, reports. User clicks Next/Prev. Best for known size and exact navigation.
  </Card>
</CardGroup>

## Convex pagination

Convex provides cursor-based pagination via `paginate`:

```typescript convex/tasks.ts theme={null}
import { paginationOptsValidator } from "convex/server";

export const listPaginated = query({
  args: {
    teamId:        v.id("teams"),
    paginationOpts: paginationOptsValidator,
  },
  handler: async (ctx, { teamId, paginationOpts }) => {
    return await ctx.db
      .query("tasks")
      .withIndex("by_team", q => q.eq("teamId", teamId))
      .order("desc")
      .paginate(paginationOpts);
  },
});
```

Returns:

```typescript theme={null}
{ page: TaskRow[], isDone: boolean, continueCursor: string }
```

## Client — infinite scroll

```tsx theme={null}
import { usePaginatedQuery } from "convex/react";

const { results, status, loadMore } = usePaginatedQuery(
  api.tasks.listPaginated,
  { teamId },
  { initialNumItems: 25 }
);

return (
  <>
    {results.map(t => <TaskRow key={t._id} task={t} />)}
    {status === "CanLoadMore" && (
      <button onClick={() => loadMore(25)}>Load more</button>
    )}
  </>
);
```

For auto-loading on scroll, attach an Intersection Observer to a sentinel element and call `loadMore` when it enters the viewport.

## Client — paginated table

Maintain page state in URL:

```tsx theme={null}
const page = parseInt(searchParams.get("page") ?? "1");
const { results } = usePaginatedQuery(
  api.tasks.listPaginated,
  { teamId },
  { initialNumItems: 25 * page }
);
const items = results.slice((page - 1) * 25, page * 25);
```

For more efficient page jumps (skip to page 5 without loading 1–4), use offset-based pagination — Convex doesn't have native offset pagination; you can implement via `_creationTime` cursors plus client-side offset.

## Tips

<Tip>
  **Pick a sane `initialNumItems`.** 25 is a good default; smaller wastes round trips, larger wastes bandwidth.
</Tip>

<Tip>
  **For truly large data, prefer search over pagination.** Users rarely scroll past page 3 — give them a search bar instead.
</Tip>

## Related

<CardGroup cols={3}>
  <Card title="Convex queries" icon="search" href="/convex/queries">
    The query layer.
  </Card>

  <Card title="Search patterns" icon="search" href="/architecture/search-patterns">
    Search as an alternative to pagination.
  </Card>

  <Card title="Performance issues" icon="gauge" href="/troubleshooting/performance-issues">
    Pagination as a fix.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.