# Pagination Sliding Active — React Pagination Component

> Numbered pagination where the active pill springs between pages; deep in the range the numbers reel through the pill instead, and each ellipsis jumps five pages.

- Category: [Pagination](https://pixelpapa.com/components/pagination.md)
- Tags: pagination, pages, layout, ellipsis, spring
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/pagination/pagination-sliding-active

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/pagination-sliding-active.json"
```

The file is written to `components/pixelpapa/pagination-sliding-active.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Ellipsis } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";

type Slot = number | "back" | "ahead";

/** Always returns the same number of slots so the control never changes width. */
export function getPageSlots(page: number, total: number, siblings = 1): Slot[] {
  const size = siblings * 2 + 5;
  if (total <= size) return Array.from({ length: total }, (_, i) => i + 1);
  const start = Math.max(Math.min(page - siblings, total - siblings * 2 - 2), 3);
  const end = Math.min(Math.max(page + siblings, siblings * 2 + 3), total - 2);
  const middle = Array.from({ length: end - start + 1 }, (_, i) => start + i);
  return [1, start > 3 ? "back" : 2, ...middle, end < total - 2 ? "ahead" : total - 1, total];
}

type PaginationProps = {
  page: number;
  total: number;
  onPageChange: (page: number) => void;
  /** Pages shown on each side of the current one. */
  siblings?: number;
  className?: string;
};

const cell =
  "relative flex size-9 items-center justify-center rounded-full text-sm tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-background";

export function Pagination({ page, total, onPageChange, siblings = 1, className }: PaginationProps) {
  const uid = useId();
  const go = (p: number) => onPageChange(Math.max(1, Math.min(total, p)));
  const slots = getPageSlots(page, total, siblings);

  return (
    <nav aria-label="Pagination" className={cn("flex items-center gap-1", className)}>
      <button
        type="button"
        aria-label="Previous page"
        disabled={page === 1}
        onClick={() => go(page - 1)}
        className={cn(cell, "text-muted-foreground hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-35")}
      >
        <ChevronLeft className="size-4" />
      </button>

      <ol className="relative flex items-center gap-1">
        <AnimatePresence mode="popLayout" initial={false}>
          {slots.map((slot) => {
            const spring = { type: "spring", stiffness: 420, damping: 34 } as const;
            if (typeof slot !== "number") {
              const Icon = slot === "back" ? ChevronsLeft : ChevronsRight;
              return (
                <motion.li key={slot} layout="position" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={spring}>
                  <button
                    type="button"
                    aria-label={slot === "back" ? "Jump back 5 pages" : "Jump ahead 5 pages"}
                    onClick={() => go(page + (slot === "back" ? -5 : 5))}
                    className={cn(cell, "group text-muted-foreground hover:bg-muted hover:text-primary")}
                  >
                    <Ellipsis className="size-4 transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0" />
                    <Icon className="absolute size-4 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" />
                  </button>
                </motion.li>
              );
            }
            const active = slot === page;
            return (
              <motion.li
                key={slot}
                layout="position"
                initial={{ opacity: 0, scale: 0.6 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0, scale: 0.6 }}
                transition={spring}
              >
                <button
                  type="button"
                  aria-label={`Page ${slot}`}
                  aria-current={active ? "page" : undefined}
                  onClick={() => go(slot)}
                  className={cn(cell, active ? "font-semibold text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground")}
                >
                  {active && (
                    <motion.span
                      layoutId={`pagination-sliding-active-${uid}`}
                      transition={{ type: "spring", stiffness: 480, damping: 34 }}
                      className="absolute inset-0 rounded-full bg-primary shadow-md shadow-primary/30"
                    />
                  )}
                  <span className="relative">{slot}</span>
                </button>
              </motion.li>
            );
          })}
        </AnimatePresence>
      </ol>

      <button
        type="button"
        aria-label="Next page"
        disabled={page === total}
        onClick={() => go(page + 1)}
        className={cn(cell, "text-muted-foreground hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-35")}
      >
        <ChevronRight className="size-4" />
      </button>
    </nav>
  );
}

export default function PaginationSlidingActiveDemo() {
  const [page, setPage] = useState(3);
  const perPage = 10;
  const results = 238;
  const total = Math.ceil(results / perPage);

  return (
    <div className="flex flex-col items-center gap-5">
      <p className="text-sm text-muted-foreground">
        Showing{" "}
        <span className="font-medium text-foreground tabular-nums">
          {(page - 1) * perPage + 1}–{Math.min(page * perPage, results)}
        </span>{" "}
        of <span className="font-medium text-foreground tabular-nums">{results}</span> orders
      </p>
      <div className="rounded-full border border-border bg-card p-1.5 shadow-sm">
        <Pagination page={page} total={total} onPageChange={setPage} />
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Pagination Sliding Active" component from PixelPapa UI to this project.

What it is: Numbered pagination where the active pill springs between pages; deep in the range the numbers reel through the pill instead, and each ellipsis jumps five pages.
Category: Pagination. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/pagination-sliding-active.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/pagination-sliding-active.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/pagination-sliding-active" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More pagination

- [Pagination Dots Morph](https://pixelpapa.com/components/pagination/pagination-dots-morph.md): Dot pagination where the active dot stretches into a pill with a spring and, on autoplay, fills like a story timer before handing over to the next dot.
- [Pagination Infinite Scroll Sentinel](https://pixelpapa.com/components/pagination/pagination-infinite-scroll-sentinel.md) (Pro): Infinite scrolling driven by an IntersectionObserver sentinel: shimmer rows appear as you near the end, new rows fade up, and a progress hairline tracks how much is loaded.
- [Pagination Jump Input](https://pixelpapa.com/components/pagination/pagination-jump-input.md) (Pro): Pagination built around an editable page field: type a number and press Enter to jump, out-of-range values shake and reset, and a scrubber shows where you are.
- [Pagination Load More](https://pixelpapa.com/components/pagination/pagination-load-more.md) (Pro): A load-more list: the button morphs into a spinner, skeleton rows hold the space, then the new items stagger in and the list scrolls to meet them.
- [Pagination Prev Next Compact](https://pixelpapa.com/components/pagination/pagination-prev-next-compact.md): A compact previous/next pager with a page-of-total readout whose number rolls up or down in the direction you are paging.
