Pagination
Numbered page buttons with previous/next controls, plus a divider-style load-more row.
Numbered page buttons with previous/next controls, plus a divider-style load-more row. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
newt-pagination"use client"
import * as React from "react"
import { Button } from "@/registry/bases/newt/ui/button"
import {
LoadMore,
Pagination,
PaginationButton,
} from "@/registry/bases/newt/ui/pagination"
const PAGES = [1, 2, 3]
export default function PaginationDemo() {
const [page, setPage] = React.useState(1)
return (
<div className="flex w-full max-w-sm flex-col gap-6">
<Pagination>
<PaginationButton
aria-label="Previous page"
disabled={page === 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
‹
</PaginationButton>
{PAGES.map((n) => (
<PaginationButton
key={n}
active={page === n}
onClick={() => setPage(n)}
>
{n}
</PaginationButton>
))}
<PaginationButton
aria-label="Next page"
disabled={page === PAGES.length}
onClick={() => setPage((p) => Math.min(PAGES.length, p + 1))}
>
›
</PaginationButton>
</Pagination>
<LoadMore>
<Button variant="secondary" size="sm">
Load more
</Button>
</LoadMore>
</div>
)
}<script setup lang="ts">
import { ref } from "vue"
import { Button } from "~~/registry/bases/newt/ui/button"
import {
LoadMore,
Pagination,
PaginationButton,
} from "~~/registry/bases/newt/ui/pagination"
const pages = [1, 2, 3]
const page = ref(1)
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-6">
<Pagination>
<PaginationButton
aria-label="Previous page"
:disabled="page === 1"
@click="page = Math.max(1, page - 1)"
>
‹
</PaginationButton>
<PaginationButton
v-for="n in pages"
:key="n"
:active="page === n"
@click="page = n"
>
{{ n }}
</PaginationButton>
<PaginationButton
aria-label="Next page"
:disabled="page === pages.length"
@click="page = Math.min(pages.length, page + 1)"
>
›
</PaginationButton>
</Pagination>
<LoadMore>
<Button variant="secondary" size="sm">Load more</Button>
</LoadMore>
</div>
</template>Installation
pnpm dlx newtui@latest add paginationnpx newtui@latest add paginationyarn dlx newtui@latest add paginationbunx newtui@latest add paginationInstall the dependencies
pnpm dlx --yes class-variance-authoritynpx --yes class-variance-authorityyarn dlx --yes class-variance-authoritybunx --yes class-variance-authorityAdd the components it builds on
Copy the source into your project
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"
const Pagination = React.forwardRef<HTMLElement, React.ComponentProps<"nav">>(
({ className, ...props }, ref) => (
<nav
ref={ref}
role="navigation"
aria-label="Pagination"
className={cn("flex items-center gap-1", className)}
{...props}
/>
)
)
Pagination.displayName = "Pagination"
const paginationButtonVariants = cva(
"flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-sm border px-2 text-[13px] font-medium disabled:cursor-not-allowed disabled:opacity-40",
{
variants: {
active: {
false:
"border-newt-border bg-newt-bg-elevated text-newt-text-secondary hover:bg-newt-bg-hover hover:text-newt-text-primary",
true: "border-newt-brand bg-newt-brand text-white",
},
},
defaultVariants: {
active: false,
},
}
)
export interface PaginationButtonProps
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof paginationButtonVariants> {}
const PaginationButton = React.forwardRef<
HTMLButtonElement,
PaginationButtonProps
>(({ className, active, ...props }, ref) => (
<button
ref={ref}
type="button"
aria-current={active ? "page" : undefined}
data-active={active ? "" : undefined}
className={cn(paginationButtonVariants({ active }), className)}
{...props}
/>
))
PaginationButton.displayName = "PaginationButton"
const PaginationEllipsis = React.forwardRef<
HTMLSpanElement,
React.ComponentProps<"span">
>(({ className, children, ...props }, ref) => (
<span
ref={ref}
aria-hidden="true"
className={cn("px-1 text-[13px] text-newt-text-muted", className)}
{...props}
>
{children ?? "…"}
</span>
))
PaginationEllipsis.displayName = "PaginationEllipsis"
/** A "load more" row: centred content flanked by hairline rules. */
const LoadMore = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-2.5 p-3 text-[13px] text-newt-text-muted before:h-px before:flex-1 before:bg-newt-border before:content-[''] after:h-px after:flex-1 after:bg-newt-border after:content-['']",
className
)}
{...props}
/>
)
)
LoadMore.displayName = "LoadMore"
export {
Pagination,
PaginationButton,
PaginationEllipsis,
LoadMore,
paginationButtonVariants,
}<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<nav
role="navigation"
aria-label="Pagination"
:class="cn('flex items-center gap-1', props.class)"
>
<slot />
</nav>
</template><script setup lang="ts">
import type { ButtonHTMLAttributes, HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { paginationButtonVariants } from "."
const props = withDefaults(
defineProps<{
active?: boolean
disabled?: ButtonHTMLAttributes["disabled"]
class?: HTMLAttributes["class"]
}>(),
{ active: false }
)
</script>
<template>
<button
type="button"
:disabled="props.disabled"
:aria-current="props.active ? 'page' : undefined"
:data-active="props.active ? '' : undefined"
:class="cn(paginationButtonVariants({ active: props.active }), props.class)"
>
<slot />
</button>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
aria-hidden="true"
:class="cn('px-1 text-[13px] text-newt-text-muted', props.class)"
>
<slot>…</slot>
</span>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
:class="
cn(
'flex items-center justify-center gap-2.5 p-3 text-[13px] text-newt-text-muted before:h-px before:flex-1 before:bg-newt-border before:content-[\'\'] after:h-px after:flex-1 after:bg-newt-border after:content-[\'\']',
props.class
)
"
>
<slot />
</div>
</template>import { cva, type VariantProps } from "class-variance-authority"
export { default as Pagination } from "./Pagination.vue"
export { default as PaginationButton } from "./PaginationButton.vue"
export { default as PaginationEllipsis } from "./PaginationEllipsis.vue"
export { default as LoadMore } from "./LoadMore.vue"
export const paginationButtonVariants = cva(
"flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-sm border px-2 text-[13px] font-medium disabled:cursor-not-allowed disabled:opacity-40",
{
variants: {
active: {
false:
"border-newt-border bg-newt-bg-elevated text-newt-text-secondary hover:bg-newt-bg-hover hover:text-newt-text-primary",
true: "border-newt-brand bg-newt-brand text-white",
},
},
defaultVariants: {
active: false,
},
}
)
export type PaginationButtonVariants = VariantProps<
typeof paginationButtonVariants
>Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { Pagination } from "@/components/ui/pagination"
export function Example() {
return <Pagination />
}<script setup lang="ts">
import { Pagination, PaginationButton, PaginationEllipsis, LoadMore } from "@/components/ui/pagination"
</script>
<template>
<Pagination />
</template>API reference
Pagination
children / default slot?React.ReactNode / Vue slot
Content rendered inside the component.
React.ReactNode / Vue slotclassName / class?string
Additional classes merged with the component defaults.
stringThe registry exports Pagination, PaginationButton, PaginationEllipsis, LoadMore. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.
Accessibility
Label the navigation region and mark the current page with aria-current="page". Previous and next controls need descriptive accessible names.
Tokens
Pagination reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-pagination.