Navigation
Link Pagination
Pagination made of ordinary links: a page can be opened in a new tab, copied and found in history. A window of numbers around the current page.
- pagination
- navigation
- links
- list
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-001?page=6&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-001" (Link 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-001.json
Registry item: https://vibeui.ru/r/pagination-001.json
Installs to: components/vibeui/pagination-001.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 made of ordinary links: a page can be opened in a new tab, copied and found in history. A window of numbers around the current page.
Link-based pagination: edge arrows, a window of numbers around the current page and gaps instead of a long list. The row does not jump between pages because the edges stay in place. Zero dependencies, one file, its own palette, no client JS.
## 3. How to use it
import { Pagination001 } from "@/components/vibeui/pagination-001"
<Pagination001
page={4}
total={12}
hrefOf={(page) => `/blog?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-001-* palette — do not swap it for your theme tokens (bg-primary, border-input and the like)
- the numbers as links rather than buttons: opening in a new tab, copying the address and going back through history all come free
- aria-current="page" on the active number: without it the active page differs only by colour
- edges that dim instead of disappearing: a vanishing arrow shifts the whole row
- tabular-nums on the numbers — without them the buttons jitter as digits change
- the aria-labels on the arrows: a screen reader would otherwise announce "←" as a symbol
- the <style> block inside the component — it holds the palette and the layout
## 6. You may change
- page and total — the current page and the count
- siblings — how many numbers surround the current page
- hrefOf — how to build a page address for your router
- the accent through the accent prop — it colours the active number
- outer spacing through className
## 7. Rules
- An infinite feed needs no pagination: they are two different ways of reading a list.
- Do not replace the links with onClick and no href: the page then cannot be opened in a new tab and vanishes from history.
- The total has to be known. Without it, show a plain "next" without 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-001.jsonhttps://vibeui.ru/r/pagination-001.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-pagination-001-*. Номера — обычные <a>, адрес собирает функция hrefOf, по умолчанию `?page=N`. Окно вокруг текущей страницы считает windowOf: первая, последняя, соседи и разрывы между ними. Текущая страница помечена 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 { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Pagination001Props = Omit<
ComponentPropsWithoutRef<"nav">,
"children"
> & {
page?: number
total?: number
/** Сколько номеров показывать вокруг текущего. */
siblings?: number
/** Как собрать адрес страницы. По умолчанию `?page=N`. */
hrefOf?: (page: number) => string
accent?: string
}
// Идея компонента: номера — обычные ссылки, а не кнопки с обработчиком.
// Страницу можно открыть в новой вкладке, скопировать и найти в истории,
// поисковик пройдёт по списку. Окно номеров вокруг текущего считается заранее,
// поэтому ширина блока не скачет при переходе.
const STYLES = `
:where([data-vibeui-block="pagination-001"]){
--vibeui-pagination-001-fg:oklch(0.26 0.016 265);
--vibeui-pagination-001-muted:oklch(0.55 0.014 265);
--vibeui-pagination-001-border:oklch(0.9 0.006 265);
--vibeui-pagination-001-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-pagination-001-accent:oklch(0.55 0.2 262);
--vibeui-pagination-001-accent-fg:oklch(1 0 0);
--vibeui-pagination-001-size:2.25rem;
--vibeui-pagination-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="pagination-001"]{font-family:var(--vibeui-pagination-001-font)}
[data-vibeui-block="pagination-001"] ul{
display:flex;align-items:center;gap:0.25rem;margin:0;padding:0;list-style:none;
}
[data-vibeui-block="pagination-001"] a,
[data-vibeui-block="pagination-001"] [data-part="gap"]{
display:inline-flex;align-items:center;justify-content:center;
min-width:var(--vibeui-pagination-001-size);height:var(--vibeui-pagination-001-size);
padding:0 0.5rem;border-radius:0.5rem;
font-size:0.875rem;line-height:1;text-decoration:none;
color:var(--vibeui-pagination-001-fg);
font-variant-numeric:tabular-nums;
transition:background-color .16s ease,color .16s ease,border-color .16s ease;
}
[data-vibeui-block="pagination-001"] a:hover{background:var(--vibeui-pagination-001-hover)}
[data-vibeui-block="pagination-001"] a:focus-visible{outline:2px solid var(--vibeui-pagination-001-accent);outline-offset:2px}
[data-vibeui-block="pagination-001"] [data-part="gap"]{color:var(--vibeui-pagination-001-muted);cursor:default}
[data-vibeui-block="pagination-001"] [aria-current="page"]{
background:var(--vibeui-pagination-001-accent);color:var(--vibeui-pagination-001-accent-fg);font-weight:600;
}
[data-vibeui-block="pagination-001"] [aria-current="page"]:hover{background:var(--vibeui-pagination-001-accent)}
[data-vibeui-block="pagination-001"] [data-part="edge"]{
border:1px solid var(--vibeui-pagination-001-border);color:var(--vibeui-pagination-001-muted);
}
/* Недоступный край остаётся на месте: без него ряд прыгает на первой странице. */
[data-vibeui-block="pagination-001"] [data-part="edge"][aria-disabled="true"]{
opacity:.4;pointer-events:none;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="pagination-001"] *{animation:none!important;transition:none!important}}
`
/** Окно номеров вокруг текущей страницы с краями и разрывами. */
function windowOf(page: number, total: number, siblings: number) {
const pages = new Set<number>([1, total])
for (let index = page - siblings; index <= page + siblings; index += 1) {
if (index >= 1 && index <= total) {
pages.add(index)
}
}
const sorted = [...pages].sort((a, b) => a - b)
const result: (number | "gap")[] = []
sorted.forEach((value, index) => {
if (index > 0 && value - sorted[index - 1] > 1) {
result.push("gap")
}
result.push(value)
})
return result
}
/**
* Постраничная навигация ссылками: окно номеров вокруг текущей страницы.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Pagination001({
page = 4,
total = 12,
siblings = 1,
hrefOf = (value) => `?page=${value}`,
accent,
className,
style,
...props
}: Pagination001Props) {
const palette = {
...(accent ? { "--vibeui-pagination-001-accent": accent } : null),
...style,
} as CSSProperties
const items = windowOf(page, total, siblings)
return (
<>
<style href="vibeui-pagination-001" precedence="medium">
{STYLES}
</style>
<nav
{...props}
data-vibeui-block="pagination-001"
aria-label="Страницы"
className={className}
style={palette}
>
<ul>
<li>
<a
data-part="edge"
href={hrefOf(Math.max(1, page - 1))}
aria-disabled={page === 1 || undefined}
aria-label="Предыдущая страница"
>
←
</a>
</li>
{items.map((item, index) =>
item === "gap" ? (
<li key={`gap-${index}`}>
<span data-part="gap" aria-hidden="true">
…
</span>
</li>
) : (
<li key={item}>
<a
href={hrefOf(item)}
aria-current={item === page ? "page" : undefined}
>
{item}
</a>
</li>
),
)}
<li>
<a
data-part="edge"
href={hrefOf(Math.min(total, page + 1))}
aria-disabled={page === total || undefined}
aria-label="Следующая страница"
>
→
</a>
</li>
</ul>
</nav>
</>
)
}