Navigation
Thumb Pagination
Pagination for a phone: three large targets instead of a row of numbers, with the full page list opening as a grid in a popover.
- pagination
- mobile
- touch
- popover
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/pagination-009?page=13&lang=en
Show the full instructions
What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.
# Install and place "pagination-009" (Thumb Pagination) from VibeUI
## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/pagination-009.json
Registry item: https://vibeui.ru/r/pagination-009.json
Installs to: components/vibeui/pagination-009.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.
Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.
## 2. What it is
Pagination for a phone: three large targets instead of a row of numbers, with the full page list opening as a grid in a popover.
Compact phone pagination: large arrows at the edges and a current-page button in the middle that opens a grid of every number. Every step is a link, so a page opens in a new tab and stays in history. Zero dependencies, one file, no client JS.
## 3. How to use it
import { Pagination009 } from "@/components/vibeui/pagination-009"
<Pagination009 page={3} total={18} hrefOf={(page) => `/catalog?page=${page}`} />
Read the installed file for the full prop list.
## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.
Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)
## 5. Keep exactly as installed
- the local --vibeui-pagination-009-* palette — do not swap it for your theme tokens (bg-card, border-input and the like)
- targets no smaller than 2.75rem: a row of small numbers cannot be hit with a thumb
- the number grid in a popover instead of a row: eighteen numbers do not fit across a narrow screen
- popover="auto": Escape and a tap outside close the grid without your code
- aria-current="page" on the current number both in the list and on the indicator button
- the aria-labels on the arrows: a border-drawn arrow has no text and is nameless without one
## 6. You may change
- page and total — the current page and the count
- hrefOf — how to build a page address for your router
- the accent through the accent prop — it colours the active number and the focus rings
- the target size and the number of grid columns
## 7. Rules
- The grid lists every page: at several hundred it becomes a long scrolling list.
- Without anchor positioning support the grid opens centred: acceptable on a phone, but check it.
- The component is meant for narrow widths: on desktop take the row of numbers.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.
## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.For developers
npx shadcn@latest add https://vibeui.ru/r/pagination-009.jsonhttps://vibeui.ru/r/pagination-009.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-pagination-009-*. Серверный: хуков нет, все номера — обычные ссылки. Цели по 2.75rem — нижняя граница для касания пальцем. Сетка страниц лежит в popover="auto" с якорем на самой панели, поэтому закрытие по Escape и по касанию мимо достаётся от браузера. Текущая страница помечена aria-current="page", края гасятся через aria-disabled.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
import type { CSSProperties } from "react"
export type Pagination009Props = {
page?: number
total?: number
hrefOf?: (page: number) => string
accent?: string
className?: string
style?: CSSProperties
}
// Идея компонента: пагинация для телефона. Ряд номеров на узком экране нечитаем
// и в него не попасть пальцем, поэтому здесь три крупные цели: назад, вперёд и
// текущая страница, открывающая сетку всех номеров. Сетка — HTML popover: она
// закрывается по Escape и по касанию мимо без единой строки JS.
const STYLES = `
:where([data-vibeui-block="pagination-009"]){
--vibeui-pagination-009-bg:oklch(1 0 0);
--vibeui-pagination-009-fg:oklch(0.24 0.014 265);
--vibeui-pagination-009-muted:oklch(0.55 0.014 265);
--vibeui-pagination-009-border:oklch(0.91 0.006 265);
--vibeui-pagination-009-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-pagination-009-accent:oklch(0.55 0.2 262);
--vibeui-pagination-009-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="pagination-009"]{
box-sizing:border-box;width:100%;max-width:20rem;padding:0.375rem;
display:flex;align-items:center;gap:0.375rem;
background:var(--vibeui-pagination-009-bg);color:var(--vibeui-pagination-009-fg);
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.875rem;
font-family:var(--vibeui-pagination-009-font);
anchor-name:--vibeui-pagination-009-bar;
}
/* Цели по 2.75rem: меньше пальцем не попасть, это нижняя граница для касания. */
[data-vibeui-block="pagination-009"] [data-part="step"]{
width:2.75rem;height:2.75rem;flex:none;
display:inline-grid;place-items:center;
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.75rem;
text-decoration:none;color:inherit;
}
[data-vibeui-block="pagination-009"] [data-part="step"]:hover{background:var(--vibeui-pagination-009-hover)}
[data-vibeui-block="pagination-009"] [data-part="step"]:focus-visible{outline:2px solid var(--vibeui-pagination-009-accent);outline-offset:2px}
[data-vibeui-block="pagination-009"] [data-part="step"][aria-disabled="true"]{opacity:.4;pointer-events:none}
[data-vibeui-block="pagination-009"] [data-part="arrow"]{
width:0.5rem;height:0.5rem;
border:1.75px solid currentColor;border-right:0;border-bottom:0;transform:rotate(-45deg);
margin-left:0.1875rem;
}
[data-vibeui-block="pagination-009"] [data-part="step"][data-dir="next"] [data-part="arrow"]{transform:rotate(135deg);margin:0 0.1875rem 0 0}
[data-vibeui-block="pagination-009"] [data-part="current"]{
flex:1;height:2.75rem;appearance:none;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;gap:0.375rem;
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.75rem;
background:none;color:inherit;font:inherit;font-size:0.9375rem;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="pagination-009"] [data-part="current"]:focus-visible{outline:2px solid var(--vibeui-pagination-009-accent);outline-offset:2px}
[data-vibeui-block="pagination-009"] [data-part="of"]{color:var(--vibeui-pagination-009-muted);font-size:0.8125rem}
[data-vibeui-block="pagination-009"] [data-part="sheet"]{
position:fixed;inset:auto;margin:0;
width:min(18rem,92vw);max-height:14rem;overflow-y:auto;
padding:0.5rem;box-sizing:border-box;
background:var(--vibeui-pagination-009-bg);color:var(--vibeui-pagination-009-fg);
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.875rem;
font-family:var(--vibeui-pagination-009-font);
box-shadow:0 24px 48px -24px oklch(0.2 0.03 265 / 40%);
}
@supports (anchor-name: --a){
[data-vibeui-block="pagination-009"] [data-part="sheet"]{
position-anchor:--vibeui-pagination-009-bar;
position-area:top span-right;margin-bottom:0.5rem;
position-try-fallbacks:flip-block;
}
}
[data-vibeui-block="pagination-009"] [data-part="grid"]{
margin:0;padding:0;list-style:none;
display:grid;grid-template-columns:repeat(auto-fill,minmax(2.5rem,1fr));gap:0.25rem;
}
[data-vibeui-block="pagination-009"] [data-part="num"]{
height:2.5rem;display:grid;place-items:center;border-radius:0.5rem;
text-decoration:none;color:inherit;font-size:0.875rem;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="pagination-009"] [data-part="num"]:hover{background:var(--vibeui-pagination-009-hover)}
[data-vibeui-block="pagination-009"] [data-part="num"]:focus-visible{outline:2px solid var(--vibeui-pagination-009-accent);outline-offset:-2px}
[data-vibeui-block="pagination-009"] [data-part="num"][aria-current="page"]{
background:var(--vibeui-pagination-009-accent);color:oklch(1 0 0);font-weight:650;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="pagination-009"] *{animation:none!important;transition:none!important}}
`
/**
* Компактная пагинация для телефона: крупные стрелки и сетка номеров.
* Один файл, ноль зависимостей, собственная палитра, клиентского JS нет.
*/
export function Pagination009({
page = 3,
total = 18,
hrefOf = (value: number) => `?page=${value}`,
accent,
className,
style,
}: Pagination009Props) {
const palette = {
...(accent ? { "--vibeui-pagination-009-accent": accent } : null),
...style,
} as CSSProperties
const first = page <= 1
const last = page >= total
return (
<>
<style href="vibeui-pagination-009" precedence="medium">
{STYLES}
</style>
<nav
data-vibeui-block="pagination-009"
aria-label="Навигация по страницам"
className={className}
style={palette}
>
<a
data-part="step"
data-dir="prev"
href={first ? undefined : hrefOf(page - 1)}
aria-disabled={first || undefined}
aria-label="Предыдущая страница"
rel="prev"
>
<span data-part="arrow" aria-hidden="true" />
</a>
<button
type="button"
data-part="current"
aria-haspopup="true"
popoverTarget="vibeui-pagination-009-sheet"
>
{page}
<span data-part="of">из {total}</span>
</button>
<a
data-part="step"
data-dir="next"
href={last ? undefined : hrefOf(page + 1)}
aria-disabled={last || undefined}
aria-label="Следующая страница"
rel="next"
>
<span data-part="arrow" aria-hidden="true" />
</a>
<div
id="vibeui-pagination-009-sheet"
data-part="sheet"
popover="auto"
aria-label="Все страницы"
>
<ul data-part="grid">
{Array.from({ length: total }, (unused, index) => index + 1).map(
(value) => (
<li key={value}>
<a
data-part="num"
href={hrefOf(value)}
aria-current={value === page ? "page" : undefined}
>
{value}
</a>
</li>
),
)}
</ul>
</div>
</nav>
</>
)
}