Pagination
Split lists and tables into pages — real links for SEO, Next.js Link support and a compact mobile view.
21–30 / 230 ta
'use client';
import { Pagination, PaginationInfo } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function PaginationBasic() {
const [page, setPage] = useState(3);
return (
<div className="flex flex-col items-center gap-3">
<Pagination page={page} onPageChange={setPage} total={230} pageSize={10} />
<PaginationInfo page={page} pageSize={10} total={230} />
</div>
);
}
Import
import { Pagination, PaginationInfo } from '@akhatmirzo/ui';When to use
- Long lists and tables — orders, blog posts, search results. Users can jump straight to the page they need.
- When the page belongs in the URL (
/blog?page=3), passgetHref: the items become real<a href>links and search engines can discover every page. - Not for infinite feeds. For a social-style feed, a "Load more" button or automatic loading works better.
- Skip it for short lists. If everything fits on one page, don't show pagination.
How pages are calculated
The page list comes from getPagination in @akhatmirzo/hooks — the same algorithm DataTable
uses. The number of items stays stable (siblings * 2 + 5), so the buttons don't jump around when
the page changes. The … is not clickable.
Pass total + pageSize (default 10) or pageCount directly. Pages start at 1.
Sizes
size — sm (32px) or md (40px, default) square buttons.
'use client';
import { Pagination } from '@akhatmirzo/ui';
export default function PaginationSizes() {
return (
<div className="flex flex-col items-center gap-4">
<Pagination defaultPage={4} pageCount={12} size="sm" compact={false} />
<Pagination defaultPage={4} pageCount={12} compact={false} />
</div>
);
}
First / last page
showFirstLast adds ⏮ ⏭ buttons. siblings controls how many numbers appear next to the current
page (default 1).
'use client';
import { Pagination } from '@akhatmirzo/ui';
export default function PaginationFirstLast() {
return <Pagination defaultPage={10} pageCount={40} siblings={2} showFirstLast compact={false} />;
}
Compact view
compact="auto" (default) switches to ‹ 3 / 24 › on screens narrower than 640px. compact
always shows the compact view, compact={false} always shows the full one.
'use client';
import { Pagination } from '@akhatmirzo/ui';
export default function PaginationCompact() {
return (
<div className="flex flex-col items-center gap-4">
<Pagination defaultPage={3} pageCount={24} compact />
<Pagination defaultPage={3} pageCount={24} compact size="sm" showFirstLast />
</div>
);
}
Why CSS?
In auto mode both views are rendered and a CSS breakpoint hides one of them (display: none, so
screen readers skip it too). There is no JavaScript matchMedia, so there is no SSR hydration
mismatch and no layout jump on load.
Links (SEO)
With getHref, every page is a real <a href>. Clicking navigates as usual and also calls
onPageChange. At the edges, "Previous" / "Next" render as disabled buttons, not links.
'use client';
import { Pagination } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function PaginationLinks() {
// Haqiqiy loyihada sahifa URL'dan o'qiladi: ?page=3
const [page, setPage] = useState(1);
return (
<Pagination page={page} onPageChange={setPage} pageCount={8} getHref={(p) => `#sahifa-${p}`} />
);
}
Next.js Link
renderLink lets you plug in your own link component. Spread all props onto the link; page is
the target page.
import Link from 'next/link';
import { Pagination } from '@akhatmirzo/ui';
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}) {
const page = Number((await searchParams).page ?? 1);
const { posts, total } = await getPosts(page);
return (
<>
<PostList posts={posts} />
<Pagination
page={page}
total={total}
pageSize={10}
getHref={(p) => `/blog?page=${p}`}
renderLink={({ page, ...props }) => <Link {...props} />}
/>
</>
);
}With DataTable
DataTable has its own pagination (the pagination prop). If you need to control paging outside the
table — for example through the URL — leave pagination out and put Pagination +
PaginationInfo below it.
| № | Mijoz | Summa |
|---|---|---|
| 3001 | Jasur Karimov | 120 000 so'm |
| 3002 | Malika Rahimova | 434 159 so'm |
| 3003 | Nodira Saidova | 748 318 so'm |
| 3004 | Otabek Qodirov | 1 062 477 so'm |
| 3005 | Jasur Karimov | 1 376 636 so'm |
1–5 / 47 ta
'use client';
import { formatMoney } from '@akhatmirzo/core';
import { DataTable, Pagination, PaginationInfo, type DataTableColumn } from '@akhatmirzo/ui';
import { useState } from 'react';
interface Order {
id: number;
customer: string;
total: number;
}
const NAMES = ['Jasur Karimov', 'Malika Rahimova', 'Nodira Saidova', 'Otabek Qodirov'];
const ORDERS: Order[] = Array.from({ length: 47 }, (_, i) => ({
id: 3001 + i,
customer: NAMES[i % NAMES.length]!,
total: 120_000 + ((i * 314_159) % 2_500_000),
}));
const columns: DataTableColumn<Order>[] = [
{ key: 'id', header: '№', width: 88 },
{ key: 'customer', header: 'Mijoz' },
{ key: 'total', header: 'Summa', numeric: true, cell: (o) => formatMoney(o.total) },
];
const PAGE_SIZE = 5;
export default function PaginationDataTable() {
const [page, setPage] = useState(1);
const rows = ORDERS.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
return (
<div className="flex w-full flex-col gap-3">
<DataTable label="Buyurtmalar" data={rows} columns={columns} className="w-full" />
<div className="flex flex-wrap items-center justify-between gap-3">
<PaginationInfo page={page} pageSize={PAGE_SIZE} total={ORDERS.length} />
<Pagination
page={page}
onPageChange={setPage}
total={ORDERS.length}
pageSize={PAGE_SIZE}
size="sm"
/>
</div>
</div>
);
}
Props
Pagination
Other <nav> attributes are passed through.
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | — | Current page, 1-based (controlled) |
defaultPage | number | 1 | Initial page |
onPageChange | (page: number) => void | — | Called when the page changes |
total | number | — | Total number of records |
pageSize | number | 10 | Records per page |
pageCount | number | — | Number of pages directly (instead of `total` / `pageSize`) |
siblings | number | 1 | Numbers shown on each side of the current page |
getHref | (page: number) => string | — | Turns items into real `<a href>` links |
renderLink | (props: PaginationLinkProps) => ReactNode | — | Custom link component such as Next `Link` (with `getHref`) |
size | 'sm' | 'md' | 'md' | Button size: 32 / 40px |
showFirstLast | boolean | false | First / last page buttons |
compact | boolean | 'auto' | 'auto' | `‹ 3 / 24 ›` view; `auto` — below 640px |
labels | { nav, previous, next, first, last, page, ellipsis, compact } | — | Screen reader texts (Uzbek by default) |
PaginationInfo
Other <p> attributes are passed through.
| Prop | Type | Default | Description |
|---|---|---|---|
pagerequired | number | — | Current page (1-based) |
pageSizerequired | number | — | Records per page |
totalrequired | number | — | Total records |
labels | { template?: (from, to, total) => ReactNode; empty?: ReactNode } | — | Defaults: "21–30 / 230 ta" and "Ma'lumot yo'q" |
Data attributes
| Attribute | Where | Value |
|---|---|---|
data-size | nav, buttons | sm · md |
data-compact | nav | auto, or present (compact) |
data-active | page button | present on the current page |
data-disabled | previous / next | present at the edges |
Accessibility
- Structure.
<nav aria-label="Sahifalash">wraps a<ul>list, so screen readers announce a navigation landmark and the number of items. - Current page has
aria-current="page". Color is not the only signal: the current button is filled. - Names. Every button has an
aria-label("5-sahifa","Keyingi sahifa"); icons arearia-hidden. The…is read as "Boshqa sahifalar". - Keyboard. Plain buttons or links:
Tabto move,Enterto activate. Disabled edge buttons are skipped byTab. - PaginationInfo uses
aria-live="polite", so the new range is announced when the page changes.