Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Pagination

Navigation component for splitting content across multiple pages with previous/next controls and page numbers.

function PaginationBasic() {
  const [currentPage, setCurrentPage] = React.useState(1);
  const totalPages = 10;
  const isMobile = useMediaQuery("(max-width: 640px)");

  // Show fewer pages on mobile
  const visiblePages = isMobile ? 3 : 5;
  const pages = Array.from({ length: Math.min(visiblePages, totalPages) }, (_, i) => i + 1);

  return (
    <Pagination className="flex-wrap">
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
        size={isMobile ? "sm" : "default"}
      >
        {isMobile ? "Prev" : "Previous"}
      </PaginationPrevious>
      {pages.map((page) => (
        <PaginationItem
          key={page}
          isActive={page === currentPage}
          onClick={() => setCurrentPage(page)}
          size={isMobile ? "sm" : "default"}
        >
          {page}
        </PaginationItem>
      ))}
      {totalPages > visiblePages && <PaginationEllipsis />}
      {totalPages > visiblePages && (
        <PaginationItem
          isActive={currentPage === totalPages}
          onClick={() => setCurrentPage(totalPages)}
          size={isMobile ? "sm" : "default"}
        >
          {totalPages}
        </PaginationItem>
      )}
      <PaginationNext
        onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
        disabled={currentPage === totalPages}
        size={isMobile ? "sm" : "default"}
      >
        {isMobile ? "Next" : "Next"}
      </PaginationNext>
    </Pagination>
  );
}

Installation

Install following dependencies:

npm install class-variance-authority lucide-react
pnpm add class-variance-authority lucide-react
yarn add class-variance-authority lucide-react
bun add class-variance-authority lucide-react

Copy and paste the following code into your project.

components/ui/pagination.tsx
"use client";

import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";

const paginationVariants = cva("flex items-center justify-center", {
  variants: {
    variant: {
      default: "gap-1",
      compact: "gap-0.5",
    },
  },
  defaultVariants: {
    variant: "default",
  },
});

const paginationItemVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap text-sm transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ",
  {
    variants: {
      variant: {
        default:
          "rounded-ele h-9 w-9 text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring",
        outline:
          "rounded-ele h-9 w-9 border border-border text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring",
        ghost:
          "rounded-ele h-9 w-9 text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring",
      },
      size: {
        default: "h-9 w-9",
        sm: "h-8 w-8 text-xs",
        lg: "h-10 w-10",
      },
      state: {
        default: "",
        active:
          "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground focus-visible:ring-ring",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
      state: "default",
    },
  }
);

const paginationNavVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-ele px-3 text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring",
  {
    variants: {
      size: {
        default: "h-9",
        sm: "h-8 text-xs px-2",
        lg: "h-10 px-4",
      },
    },
    defaultVariants: {
      size: "default",
    },
  }
);

export interface PaginationProps
  extends React.HTMLAttributes<HTMLElement>,
    VariantProps<typeof paginationVariants> {}

export interface PaginationItemProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof paginationItemVariants> {
  isActive?: boolean;
}

export interface PaginationNavProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof paginationNavVariants> {}

export interface PaginationEllipsisProps
  extends React.HTMLAttributes<HTMLSpanElement> {}

const Pagination = React.forwardRef<HTMLElement, PaginationProps>(
  ({ className, variant, ...props }, ref) => (
    <nav
      role="navigation"
      aria-label="pagination"
      className={cn(paginationVariants({ variant, className }))}
      ref={ref}
      {...props}
    />
  )
);
Pagination.displayName = "Pagination";

const PaginationItem = React.forwardRef<HTMLButtonElement, PaginationItemProps>(
  ({ className, variant, size, state, isActive, ...props }, ref) => (
    <button
      className={cn(
        paginationItemVariants({
          variant,
          size,
          state: isActive ? "active" : state,
          className,
        })
      )}
      ref={ref}
      aria-current={isActive ? "page" : undefined}
      {...props}
    />
  )
);
PaginationItem.displayName = "PaginationItem";

const PaginationPrevious = React.forwardRef<
  HTMLButtonElement,
  PaginationNavProps
>(({ className, size, children, ...props }, ref) => (
  <button
    className={cn(paginationNavVariants({ size, className }))}
    ref={ref}
    {...props}
  >
    <ChevronLeft className="h-4 w-4" />
    {children || "Previous"}
  </button>
));
PaginationPrevious.displayName = "PaginationPrevious";

const PaginationNext = React.forwardRef<HTMLButtonElement, PaginationNavProps>(
  ({ className, size, children, ...props }, ref) => (
    <button
      className={cn(paginationNavVariants({ size, className }))}
      ref={ref}
      {...props}
    >
      {children || "Next"}
      <ChevronRight className="h-4 w-4" />
    </button>
  )
);
PaginationNext.displayName = "PaginationNext";

const PaginationEllipsis = React.forwardRef<
  HTMLSpanElement,
  PaginationEllipsisProps
>(({ className, ...props }, ref) => (
  <span
    className={cn(
      "inline-flex items-center justify-center h-9 w-9 text-muted-foreground",
      className
    )}
    ref={ref}
    {...props}
  >
    <MoreHorizontal className="h-4 w-4" />
    <span className="sr-only">More pages</span>
  </span>
));
PaginationEllipsis.displayName = "PaginationEllipsis";

export {
  Pagination,
  PaginationItem,
  PaginationPrevious,
  PaginationNext,
  PaginationEllipsis,
  paginationVariants,
  paginationItemVariants,
  paginationNavVariants,
};
npx hextaui@latest add pagination
pnpm dlx hextaui@latest add pagination
yarn dlx hextaui@latest add pagination
bun x hextaui@latest add pagination

Usage

import {
  Pagination,
  PaginationItem,
  PaginationPrevious,
  PaginationNext,
  PaginationEllipsis,
} from "@/components/ui/pagination";
function PaginationExample() {
  const [currentPage, setCurrentPage] = React.useState(1);
  const totalPages = 10;

  return (
    <Pagination>
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
      >
        Previous
      </PaginationPrevious>
      {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
        <PaginationItem
          key={page}
          isActive={page === currentPage}
          onClick={() => setCurrentPage(page)}
        >
          {page}
        </PaginationItem>
      ))}
      <PaginationEllipsis />
      <PaginationNext
        onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
        disabled={currentPage === totalPages}
      >
        Next
      </PaginationNext>
    </Pagination>
  );
}

Examples

Responsive Design

The pagination component is designed to be responsive and adapts to different screen sizes:

  • Mobile (< 640px): Uses smaller buttons, fewer visible pages, and shorter labels
  • Tablet (< 768px): Reduced delta for ellipsis calculation
  • Desktop: Full feature set with optimal spacing

The responsive behavior is implemented using a custom useMediaQuery hook that detects screen size changes.

Basic Pagination

// Hook to detect screen size
function useMediaQuery(query: string) {
  const [matches, setMatches] = React.useState(false);

  React.useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) {
      setMatches(media.matches);
    }
    const listener = () => setMatches(media.matches);
    media.addListener(listener);
    return () => media.removeListener(listener);
  }, [matches, query]);

  return matches;
}

function PaginationBasic() {
  const [currentPage, setCurrentPage] = React.useState(1);
  const totalPages = 10;
  const isMobile = useMediaQuery("(max-width: 640px)");

  // Show fewer pages on mobile
  const visiblePages = isMobile ? 3 : 5;
  const pages = Array.from({ length: Math.min(visiblePages, totalPages) }, (_, i) => i + 1);

  return (
    <Pagination className="flex-wrap">
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
        size={isMobile ? "sm" : "default"}
      >
        {isMobile ? "Prev" : "Previous"}
      </PaginationPrevious>
      {pages.map((page) => (
        <PaginationItem
          key={page}
          isActive={page === currentPage}
          onClick={() => setCurrentPage(page)}
          size={isMobile ? "sm" : "default"}
        >
          {page}
        </PaginationItem>
      ))}
      {totalPages > visiblePages && <PaginationEllipsis />}
      {totalPages > visiblePages && (
        <PaginationItem
          isActive={currentPage === totalPages}
          onClick={() => setCurrentPage(totalPages)}
          size={isMobile ? "sm" : "default"}
        >
          {totalPages}
        </PaginationItem>
      )}
      <PaginationNext
        onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
        disabled={currentPage === totalPages}
        size={isMobile ? "sm" : "default"}
      >
        {isMobile ? "Next" : "Next"}
      </PaginationNext>
    </Pagination>
  );
}

With Ellipsis

function PaginationWithEllipsis() {
  const [currentPage, setCurrentPage] = React.useState(5);
  const totalPages = 20;
  const isMobile = useMediaQuery("(max-width: 640px)");
  const isTablet = useMediaQuery("(max-width: 768px)");      const getVisiblePages = () => {
    // Adjust delta based on screen size
    const delta = isMobile ? 1 : isTablet ? 1 : 2;
    const rangeWithDots = [];

    // Special case: if total pages <= 7, show all pages
    if (totalPages <= 7) {
      return Array.from({ length: totalPages }, (_, i) => i + 1);
    }

    // Always show first page
    rangeWithDots.push(1);

    // Calculate the range around current page
    let startPage = Math.max(2, currentPage - delta);
    let endPage = Math.min(totalPages - 1, currentPage + delta);

    // Ensure current page is ALWAYS included in the range
    if (currentPage === 1) {
      // Current page is first page, extend range to the right
      endPage = Math.min(totalPages - 1, 1 + (delta * 2));
    } else if (currentPage === totalPages) {
      // Current page is last page, extend range to the left
      startPage = Math.max(2, totalPages - (delta * 2));
    } else {
      // Current page is in the middle, ensure it's in the range
      startPage = Math.max(2, Math.min(startPage, currentPage));
      endPage = Math.min(totalPages - 1, Math.max(endPage, currentPage));
    }

    // Add ellipsis after first page if there's a gap
    if (startPage > 2) {
      rangeWithDots.push("...");
    }

    // Add pages around current page (ensuring current page is always included)
    for (let i = startPage; i <= endPage; i++) {
      if (i !== 1 && i !== totalPages) {
        rangeWithDots.push(i);
      }
    }

    // Add ellipsis before last page if there's a gap
    if (endPage < totalPages - 1) {
      rangeWithDots.push("...");
    }

    // Always show last page if it's different from first
    if (totalPages > 1) {
      rangeWithDots.push(totalPages);
    }

    return rangeWithDots;
  };

  return (
    <div className="w-full overflow-x-auto">
      <Pagination className="flex-wrap min-w-fit">
        <PaginationPrevious
          onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
          disabled={currentPage === 1}
          size={isMobile ? "sm" : "default"}
        >
          {isMobile ? "Prev" : "Previous"}
        </PaginationPrevious>
        {getVisiblePages().map((page, index) =>
          page === "..." ? (
            <PaginationEllipsis key={`ellipsis-${index}`} />
          ) : (
            <PaginationItem
              key={page}
              isActive={page === currentPage}
              onClick={() => setCurrentPage(page as number)}
              size={isMobile ? "sm" : "default"}
            >
              {page}
            </PaginationItem>
          ),
        )}
        <PaginationNext
          onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
          disabled={currentPage === totalPages}
          size={isMobile ? "sm" : "default"}
        >
          {isMobile ? "Next" : "Next"}
        </PaginationNext>
      </Pagination>
    </div>
  );
}

Compact

function PaginationCompact() {
  const [currentPage, setCurrentPage] = React.useState(3);
  const totalPages = 8;
  const isMobile = useMediaQuery("(max-width: 640px)");

  return (
    <div className="flex items-center gap-2 sm:gap-4">
      <Pagination variant="compact" className="flex-wrap">
        <PaginationPrevious
          size={isMobile ? "sm" : "default"}
          onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
          disabled={currentPage === 1}
        >
          {isMobile ? "‹" : "Previous"}
        </PaginationPrevious>
        <div className="flex items-center px-2 sm:px-3">
          <span className="text-xs sm:text-sm text-muted-foreground">
            {isMobile ? `${currentPage}/${totalPages}` : `Page ${currentPage} of ${totalPages}`}
          </span>
        </div>
        <PaginationNext
          size={isMobile ? "sm" : "default"}
          onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
          disabled={currentPage === totalPages}
        >
          {isMobile ? "›" : "Next"}
        </PaginationNext>
      </Pagination>
    </div>
  );
}

Simple

function PaginationSimple() {
  const [currentPage, setCurrentPage] = React.useState(1);
  const totalPages = 5;

  return (
    <Pagination>
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
      />
      {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
        <PaginationItem
          key={page}
          isActive={page === currentPage}
          onClick={() => setCurrentPage(page)}
        >
          {page}
        </PaginationItem>
      ))}
      <PaginationNext
        onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
        disabled={currentPage === totalPages}
      />
    </Pagination>
  );
}

Minimal

function PaginationMinimal() {
  const [currentPage, setCurrentPage] = React.useState(2);
  const totalPages = 10;

  return (
    <Pagination>
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
      >
        Prev
      </PaginationPrevious>
      <div className="flex items-center px-4">
        <span className="text-sm text-muted-foreground">
          {currentPage} / {totalPages}
        </span>
      </div>
      <PaginationNext
        onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
        disabled={currentPage === totalPages}
      >
        Next
      </PaginationNext>
    </Pagination>
  );
}

With Input

Go to:
function PaginationWithInput() {
const [currentPage, setCurrentPage] = React.useState(5);
const [inputPage, setInputPage] = React.useState("5");
const totalPages = 25;
const isMobile = useMediaQuery("(max-width: 640px)");
const isTablet = useMediaQuery("(max-width: 768px)");

const handleGoToPage = () => {
const page = parseInt(inputPage);
if (page >= 1 && page <= totalPages) {
  setCurrentPage(page);
}
};

const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
  handleGoToPage();
}
};

if (isMobile) {
// Mobile layout: Stack vertically
return (
  <div className="flex flex-col gap-4 w-full">
    <Pagination className="flex-wrap">
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
        size="sm"
      >
        Prev
      </PaginationPrevious>
      <PaginationItem isActive size="sm">
        {currentPage}
      </PaginationItem>
      <PaginationNext
        onClick={() =>
          setCurrentPage(Math.min(totalPages, currentPage + 1))
        }
        disabled={currentPage === totalPages}
        size="sm"
      >
        Next
      </PaginationNext>
    </Pagination>
    <div className="flex items-center justify-center gap-2">
      <span className="text-sm text-muted-foreground">
        Go to:
      </span>
      <Input
        type="number"
        min="1"
        max={totalPages}
        value={inputPage}
        onChange={(e) => setInputPage(e.target.value)}
        onKeyPress={handleKeyPress}
        className="w-16 h-8 px-2 text-sm "
      />
      <Button
        onClick={handleGoToPage}
        className="h-8 px-3 text-sm font-medium"
      >
        Go
      </Button>
    </div>
  </div>
);
}

return (
<div className="flex flex-col items-center gap-4 w-full">
  <div className="overflow-x-auto">
    <Pagination className="flex-wrap min-w-fit">
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
        size={isTablet ? "sm" : "default"}
      >
        {isTablet ? "Prev" : "Previous"}
      </PaginationPrevious>
      <PaginationItem
        onClick={() => setCurrentPage(1)}
        isActive={currentPage === 1}
        size={isTablet ? "sm" : "default"}
      >
        1
      </PaginationItem>
      {currentPage > 3 && <PaginationEllipsis />}
      {currentPage > 2 && currentPage < totalPages && (
        <PaginationItem isActive size={isTablet ? "sm" : "default"}>
          {currentPage}
        </PaginationItem>
      )}
      {currentPage < totalPages - 2 && <PaginationEllipsis />}
      <PaginationItem
        onClick={() => setCurrentPage(totalPages)}
        isActive={currentPage === totalPages}
        size={isTablet ? "sm" : "default"}
      >
        {totalPages}
      </PaginationItem>
      <PaginationNext
        onClick={() =>
          setCurrentPage(Math.min(totalPages, currentPage + 1))
        }
        disabled={currentPage === totalPages}
        size={isTablet ? "sm" : "default"}
      >
        {isTablet ? "Next" : "Next"}
      </PaginationNext>
    </Pagination>
  </div>
  <div className="flex items-center gap-2 whitespace-nowrap">
    <span className="text-sm text-muted-foreground">
      Go to:
    </span>
    <Input
      type="number"
      min="1"
      max={totalPages}
      value={inputPage}
      onChange={(e) => setInputPage(e.target.value)}
      onKeyPress={handleKeyPress}
      className="w-16 h-8 px-2 text-sm"
    />
    <Button
      onClick={handleGoToPage}
      className="h-8 px-3 text-sm font-medium"
    >
      Go
    </Button>
  </div>
</div>
);
}

Sizes

function PaginationSizes() {
  const [currentPage, setCurrentPage] = React.useState(2);
  const totalPages = 5;

  return (
    <div className="space-y-6">
      {/* Small */}
      <div>
        <h4 className="text-sm font-medium mb-2">Small</h4>
        <Pagination>
          <PaginationPrevious
            size="sm"
            onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
            disabled={currentPage === 1}
          />
          {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
            <PaginationItem
              key={page}
              size="sm"
              isActive={page === currentPage}
              onClick={() => setCurrentPage(page)}
            >
              {page}
            </PaginationItem>
          ))}
          <PaginationNext
            size="sm"
            onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
            disabled={currentPage === totalPages}
          />
        </Pagination>
      </div>

      {/* Default */}
      <div>
        <h4 className="text-sm font-medium mb-2">Default</h4>
        <Pagination>
          <PaginationPrevious
            onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
            disabled={currentPage === 1}
          />
          {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
            <PaginationItem
              key={page}
              isActive={page === currentPage}
              onClick={() => setCurrentPage(page)}
            >
              {page}
            </PaginationItem>
          ))}
          <PaginationNext
            onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
            disabled={currentPage === totalPages}
          />
        </Pagination>
      </div>

      {/* Large */}
      <div>
        <h4 className="text-sm font-medium mb-2">Large</h4>
        <Pagination>
          <PaginationPrevious
            size="lg"
            onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
            disabled={currentPage === 1}
          />
          {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
            <PaginationItem
              key={page}
              size="lg"
              isActive={page === currentPage}
              onClick={() => setCurrentPage(page)}
            >
              {page}
            </PaginationItem>
          ))}
          <PaginationNext
            size="lg"
            onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
            disabled={currentPage === totalPages}
          />
        </Pagination>
      </div>
    </div>
  );
}

Outlined

function PaginationOutlined() {
  const [currentPage, setCurrentPage] = React.useState(2);
  const totalPages = 6;

  return (
    <Pagination>
      <PaginationPrevious
        onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
        disabled={currentPage === 1}
      />
      {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
        <PaginationItem
          key={page}
          variant="outline"
          isActive={page === currentPage}
          onClick={() => setCurrentPage(page)}
        >
          {page}
        </PaginationItem>
      ))}
      <PaginationNext
        onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
        disabled={currentPage === totalPages}
      />
    </Pagination>
  );
}

API Reference

useMediaQuery Hook

A utility hook for responsive design that detects screen size changes:

function useMediaQuery(query: string): boolean;

Parameters:

  • query: A CSS media query string (e.g., "(max-width: 640px)")

Returns:

  • boolean: Whether the media query matches

Pagination

PropTypeDefault
className?
string
undefined
variant?
"default" | "compact"
"default"

PaginationItem

PropTypeDefault
disabled?
boolean
false
onClick?
() => void
undefined
isActive?
boolean
false
size?
"default" | "sm" | "lg"
"default"
variant?
"default" | "outline" | "ghost"
"default"

PaginationPrevious / PaginationNext

PropTypeDefault
children?
React.ReactNode
"Previous" / "Next"
disabled?
boolean
false
onClick?
() => void
undefined
size?
"default" | "sm" | "lg"
"default"

PaginationEllipsis

PropTypeDefault
className?
string
undefined
Edit on GitHub

Last updated on