Pagination Stack
A collection of 7 premium pagination variants featuring spring indicators, neon glows, and liquid blur effects.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add pagination-stackSource Code
'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import PaginationClassic from './pagination'
import PaginationFramer from './framer-pagination'
import PaginationNeon from './neon-pagination'
import PaginationLiquid from './liquid-pagination'
import PaginationCapsule from './capsule-pagination'
import PaginationScroll from './scroll-pagination'
import PaginationSlider from './slider-pagination'
import PaginationAurora from './aurora-pagination'
export default function PaginationStack() {
const [copied, setCopied] = useState<string | null>(null)
const copy = (name: string, cmd: string) => {
navigator.clipboard.writeText(cmd)
setCopied(name)
setTimeout(() => setCopied(null), 1500)
}
const variants = [
{ name: 'Classic', Component: PaginationClassic, cmd: 'npx vui-registry-cli-v1 add pagination' },
{ name: 'Framer', Component: PaginationFramer, cmd: 'npx vui-registry-cli-v1 add framer-pagination' },
{ name: 'Neon', Component: PaginationNeon, cmd: 'npx vui-registry-cli-v1 add neon-pagination' },
{ name: 'Liquid', Component: PaginationLiquid, cmd: 'npx vui-registry-cli-v1 add liquid-pagination' },
{ name: 'Capsule', Component: PaginationCapsule, cmd: 'npx vui-registry-cli-v1 add capsule-pagination' },
{ name: 'Aurora', Component: PaginationAurora, cmd: 'npx vui-registry-cli-v1 add aurora-pagination' },
{ name: 'Scroll', Component: PaginationScroll, cmd: 'npx vui-registry-cli-v1 add scroll-pagination' },
{ name: 'Slider', Component: PaginationSlider, cmd: 'npx vui-registry-cli-v1 add slider-pagination' },
]
return (
<div className="w-full grid gap-6 md:grid-cols-2 p-4">
{variants.map((v) => (
<div key={v.name} className={`rounded-2xl p-[2px] border border-border bg-card/50 backdrop-blur-sm overflow-hidden ${v.name === 'Slider' ? 'md:col-span-2' : ''}`}>
<div className="rounded-xl border border-border bg-background/60">
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-muted/10 relative">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{v.name}</div>
<button
onClick={() => copy(v.name, v.cmd)}
className="text-[10px] font-mono border-b border-border absolute top-2 right-3"
>
<AnimatePresence mode="wait">
{copied === v.name ? (
<motion.span key="copied" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="text-emerald-500">
Copied
</motion.span>
) : (
<motion.span key="copy" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
CLI
</motion.span>
)}
</AnimatePresence>
</button>
</div>
<div className="p-6 min-h-[160px] grid place-items-center">
<v.Component />
</div>
</div>
</div>
))}
</div>
)
}
Source Code
'use client'
import React, { useState } from 'react'
import { motion } from 'framer-motion'
import { ChevronLeft, ChevronRight } from 'lucide-react'
export default function PaginationClassic() {
const [page, setPage] = useState(1)
const total = 7
const pages = Array.from({ length: total }, (_, i) => i + 1)
return (
<div className="flex items-center justify-center p-6">
<div className="flex items-center gap-1 p-1.5 bg-background/50 backdrop-blur-sm rounded-2xl border border-border shadow-sm">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="p-2.5 rounded-xl hover:bg-foreground/5 disabled:opacity-30 transition-all active:scale-95"
>
<ChevronLeft size={16} className="text-muted-foreground" />
</button>
<div className="flex items-center relative gap-1">
{pages.map((p, i) => (
<button
key={p}
onClick={() => setPage(p)}
className={`relative w-9 h-9 flex items-center justify-center text-sm font-medium rounded-xl z-10 transition-colors duration-300 ${
page === p ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-foreground/5'
}`}
>
{page === p && (
<motion.div
layoutId="pagination-classic"
className="absolute inset-0 rounded-xl bg-primary shadow-lg shadow-primary/20"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span className="relative">{p}</span>
</button>
))}
</div>
<button
onClick={() => setPage((p) => Math.min(total, p + 1))}
disabled={page === total}
className="p-2.5 rounded-xl hover:bg-foreground/5 disabled:opacity-30 transition-all active:scale-95"
>
<ChevronRight size={16} className="text-muted-foreground" />
</button>
</div>
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function PaginationCapsule() {
const [page, setPage] = useState(1)
const total = 7
const pages = Array.from({ length: total }, (_, i) => i + 1)
const w = 44
return (
<div className="flex items-center justify-center p-6">
<div className="relative px-2 py-2 rounded-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 shadow-inner">
<div className="relative flex items-center">
<motion.div
layout
className="absolute top-0 bottom-0 rounded-full bg-white dark:bg-zinc-800 shadow-[0_2px_10px_rgba(0,0,0,0.1)] dark:shadow-[0_2px_10px_rgba(0,0,0,0.3)] border border-zinc-200 dark:border-zinc-700"
style={{ width: w, left: (page - 1) * w }}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
{pages.map((p) => (
<button
key={p}
onClick={() => setPage(p)}
className={`relative w-[${w}px] h-10 grid place-items-center rounded-full z-10 transition-colors duration-200 ${
page === p ? 'text-black dark:text-white font-semibold' : 'text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
}`}
>
<span className="text-sm">{p}</span>
</button>
))}
</div>
</div>
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function PaginationFramer() {
const [page, setPage] = useState(1)
const total = 6
const pages = Array.from({ length: total }, (_, i) => i + 1)
return (
<div className="flex items-center justify-center p-6">
<div className="flex items-center gap-2 px-3 py-2 bg-background/40 backdrop-blur-md rounded-2xl border border-border/60 shadow-sm">
<div className="relative flex items-center gap-1">
{pages.map((p, i) => (
<button
key={p}
onClick={() => setPage(p)}
className="relative w-10 h-10 grid place-items-center rounded-xl z-10"
>
<motion.span
animate={{ scale: page === p ? 1.1 : 0.9, opacity: page === p ? 1 : 0.6 }}
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
className={`text-sm font-bold ${
page === p ? 'text-primary' : 'text-muted-foreground hover:text-foreground hover:scale-105 transition-all'
}`}
>
{p}
</motion.span>
</button>
))}
<motion.div
layout
className="absolute top-0 bottom-0 rounded-xl bg-primary/10 border border-primary/20 shadow-[0_0_20px_rgba(var(--primary),0.2)]"
style={{ width: 40, left: (page - 1) * 44 }}
transition={{ type: 'spring', stiffness: 350, damping: 25 }}
/>
</div>
</div>
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function PaginationLiquid() {
const [page, setPage] = useState(1)
const total = 6
const pages = Array.from({ length: total }, (_, i) => i + 1)
return (
<div className="flex items-center justify-center p-6">
<div className="relative flex items-center gap-2 px-3 py-2 rounded-2xl bg-white dark:bg-neutral-900 border border-black/5 dark:border-white/10 shadow-sm overflow-hidden">
<motion.div
layout
className="absolute rounded-full blur-xl bg-blue-500/30 dark:bg-blue-500/20 mix-blend-multiply dark:mix-blend-screen"
style={{ width: 60, height: 60, left: (page - 1) * 44 - 6, top: -6 }}
transition={{ type: 'spring', stiffness: 200, damping: 25 }}
/>
{pages.map((p) => (
<button
key={p}
onClick={() => setPage(p)}
className={`relative w-9 h-9 grid place-items-center rounded-xl z-10 transition-colors duration-300 ${
page === p ? 'text-blue-600 dark:text-blue-400 font-bold bg-blue-50/50 dark:bg-blue-900/20' : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-neutral-300'
}`}
>
<span className="text-sm">{p}</span>
</button>
))}
</div>
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function PaginationNeon() {
const [page, setPage] = useState(1)
const total = 6
const pages = Array.from({ length: total }, (_, i) => i + 1)
return (
<div className="flex items-center justify-center p-6 bg-black rounded-3xl">
<div className="relative flex items-center gap-2 px-3 py-3 rounded-2xl bg-neutral-900/80 border border-neutral-800 shadow-2xl shadow-black/50 backdrop-blur-sm">
{pages.map((p) => (
<button
key={p}
onClick={() => setPage(p)}
className={`relative w-10 h-10 grid place-items-center rounded-xl transition-colors duration-300 z-10 ${
page === p ? 'text-white font-bold' : 'text-neutral-500 hover:text-neutral-300'
}`}
>
<span className="relative z-10">{p}</span>
{page === p && (
<motion.div
layoutId="neon-glow"
className="absolute inset-0 rounded-xl bg-indigo-500/20 border border-indigo-500/50"
style={{ boxShadow: '0 0 20px 2px rgba(99,102,241,0.4), inset 0 0 10px rgba(99,102,241,0.2)' }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
/>
)}
</button>
))}
</div>
</div>
)
}
Source Code
'use client'
import { useRef, useState } from 'react'
import { motion } from 'framer-motion'
import { ChevronLeft, ChevronRight } from 'lucide-react'
export default function PaginationScroll() {
const [page, setPage] = useState(1)
const total = 8
const scroller = useRef<HTMLDivElement | null>(null)
const pages = Array.from({ length: total }, (_, i) => i + 1)
function go(dir: -1 | 1) {
const next = Math.min(total, Math.max(1, page + dir))
setPage(next)
const el = scroller.current
if (el) el.scrollTo({ left: (next - 1) * 48, behavior: 'smooth' })
}
return (
<div className="flex items-center justify-center p-6">
<div className="flex items-center gap-2">
<button
onClick={() => go(-1)}
disabled={page === 1}
className="p-3 rounded-xl border border-border/50 bg-background/50 backdrop-blur-sm text-muted-foreground hover:bg-foreground/5 hover:text-foreground disabled:opacity-30 transition-all active:scale-95"
>
<ChevronLeft size={16} />
</button>
<div className="relative p-1 rounded-2xl border border-border/50 bg-background/30 backdrop-blur-sm shadow-inner">
<div
ref={scroller}
className="flex items-center overflow-x-auto scroll-smooth snap-x snap-mandatory gap-2 px-1 py-1 no-scrollbar w-[240px]"
>
{pages.map((p) => (
<button
key={p}
onClick={() => setPage(p)}
className={`w-10 h-10 flex-shrink-0 grid place-items-center rounded-xl snap-center transition-all duration-300 ${
page === p
? 'bg-primary text-primary-foreground font-bold shadow-md scale-100'
: 'text-muted-foreground hover:bg-foreground/5 scale-90 opacity-70 hover:opacity-100'
}`}
>
{p}
</button>
))}
</div>
<motion.div
className="absolute top-1/2 -translate-y-1/2 rounded-xl bg-primary/10 border border-primary/20 pointer-events-none"
style={{ width: 44, height: 44, left: (page - 1) * 48 + 4 }}
animate={{ left: (page - 1) * 48 + 4 }}
transition={{ type: 'spring', stiffness: 350, damping: 25 }}
/>
<div className="absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-background to-transparent pointer-events-none" />
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-background to-transparent pointer-events-none" />
</div>
<button
onClick={() => go(1)}
disabled={page === total}
className="p-3 rounded-xl border border-border/50 bg-background/50 backdrop-blur-sm text-muted-foreground hover:bg-foreground/5 hover:text-foreground disabled:opacity-30 transition-all active:scale-95"
>
<ChevronRight size={16} />
</button>
</div>
<style jsx>{`
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
`}</style>
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function PaginationSlider() {
const [page, setPage] = useState(1)
const total = 10
const pages = Array.from({ length: total }, (_, i) => i + 1)
const progress = (page - 1) / (total - 1)
return (
<div className="flex flex-col items-center justify-center p-8 gap-6 w-full max-w-md mx-auto">
<div className="relative w-full group">
<div className="absolute top-1/2 left-0 right-0 h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-full -translate-y-1/2" />
<motion.div
className="absolute top-1/2 left-0 h-1.5 bg-primary rounded-full -translate-y-1/2 origin-left"
initial={{ scaleX: progress }}
animate={{ scaleX: progress }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{ width: '100%' }}
/>
<input
type="range"
min={1}
max={total}
value={page}
onChange={(e) => setPage(parseInt(e.target.value))}
className="relative w-full h-8 opacity-0 cursor-pointer z-10"
/>
<motion.div
className="absolute top-1/2 w-6 h-6 bg-white dark:bg-neutral-900 border-2 border-primary rounded-full shadow-lg -translate-y-1/2 -translate-x-1/2 pointer-events-none"
animate={{ left: `${progress * 100}%`, scale: 1 }}
whileHover={{ scale: 1.08 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 350, damping: 25 }}
/>
<motion.div
className="absolute top-1/2 w-12 h-12 rounded-full -translate-y-1/2 -translate-x-1/2 pointer-events-none"
animate={{ left: `${progress * 100}%`, opacity: 0.5 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{ boxShadow: '0 0 30px 6px rgba(var(--primary), 0.25)' }}
/>
</div>
<div className="flex items-center justify-between w-full px-1">
{pages.map((p) => (
<button
key={p}
onClick={() => setPage(p)}
className={`w-8 h-8 grid place-items-center rounded-lg text-xs font-medium transition-all duration-200 ${
page === p
? 'bg-primary text-primary-foreground shadow-md -translate-y-1 scale-110'
: 'text-muted-foreground hover:bg-neutral-100 dark:hover:bg-neutral-800'
}`}
>
{p}
</button>
))}
</div>
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function PaginationAurora() {
const [page, setPage] = useState(1)
const total = 7
const pages = Array.from({ length: total }, (_, i) => i + 1)
const w = 44
return (
<div className="flex items-center justify-center p-6">
<div className="relative flex items-center gap-2 px-3 py-2 rounded-2xl border border-border bg-background/50 backdrop-blur-md">
<motion.div
layout
className="absolute rounded-2xl"
style={{ width: w, height: 44, left: (page - 1) * (w + 4) + 2 }}
transition={{ type: 'spring', stiffness: 320, damping: 26 }}
>
<motion.div
className="absolute inset-0 rounded-xl"
style={{
background:
'conic-gradient(from 180deg, rgba(99,102,241,0.25), rgba(34,197,94,0.25), rgba(244,63,94,0.25), rgba(99,102,241,0.25))',
filter: 'blur(16px)',
}}
animate={{ opacity: 0.7 }}
transition={{ duration: 0.3 }}
/>
<motion.div
className="absolute inset-0 rounded-xl bg-primary/10 border border-primary/20"
/>
</motion.div>
{pages.map((p) => (
<button
key={p}
onClick={() => setPage(p)}
className={`relative w-11 h-11 grid place-items-center rounded-xl z-10 transition-all duration-200 ${
page === p
? 'text-foreground font-semibold'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<motion.span
animate={{ scale: page === p ? 1.05 : 0.95, opacity: page === p ? 1 : 0.75 }}
transition={{ type: 'spring', stiffness: 400, damping: 24 }}
className="text-sm"
>
{p}
</motion.span>
</button>
))}
</div>
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| currentPage | number | 1 | The currently active page. |
| totalPages | number | 10 | Total number of pages available. |
| onPageChange | (page: number) => void | undefined | Callback function when a page is selected. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

