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

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 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.json
https://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>
    </>
  )
}