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 font-medium 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-[var(--radius)] h-9 w-9 text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus-visible:ring-[hsl(var(--hu-ring))]",
        outline:
          "rounded-[var(--radius)] h-9 w-9 border border-[hsl(var(--hu-border))] text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus-visible:ring-[hsl(var(--hu-ring))]",
        ghost:
          "rounded-[var(--radius)] h-9 w-9 text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus-visible:ring-[hsl(var(--hu-ring))]",
      },
      size: {
        default: "h-9 w-9",
        sm: "h-8 w-8 text-xs",
        lg: "h-10 w-10",
      },
      state: {
        default: "",
        active:
          "bg-[hsl(var(--hu-primary))] text-[hsl(var(--hu-primary-foreground))] hover:bg-[hsl(var(--hu-primary))]/90",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
      state: "default",
    },
  },
);

const paginationNavVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-[var(--radius)] px-3 text-[hsl(var(--hu-foreground))] hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))] focus-visible:ring-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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 handleGoToPage = () => {
    const page = parseInt(inputPage);
    if (page >= 1 && page <= totalPages) {
      setCurrentPage(page);
    }
  };

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

  return (
    <div className="flex items-center gap-4">
      <Pagination>
        <PaginationPrevious
          onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
          disabled={currentPage === 1}
        />
        <PaginationItem
          onClick={() => setCurrentPage(1)}
          isActive={currentPage === 1}
        >
          1
        </PaginationItem>
        {currentPage > 3 && <PaginationEllipsis />}
        {currentPage > 2 && currentPage < totalPages && (
          <PaginationItem isActive>
            {currentPage}
          </PaginationItem>
        )}
        {currentPage < totalPages - 2 && <PaginationEllipsis />}
        <PaginationItem
          onClick={() => setCurrentPage(totalPages)}
          isActive={currentPage === totalPages}
        >
          {totalPages}
        </PaginationItem>
        <PaginationNext
          onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
          disabled={currentPage === totalPages}
        />
      </Pagination>
      <div className="flex items-center gap-2">
        <span className="text-sm text-[hsl(var(--hu-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 border border-[hsl(var(--hu-border))] rounded-lg bg-[hsl(var(--hu-background))] text-[hsl(var(--hu-foreground))] focus:outline-none focus:ring-2 focus:ring-[hsl(var(--hu-ring))]"
        />
        <button
          onClick={handleGoToPage}
          className="h-8 px-3 text-sm font-medium rounded-lg bg-[hsl(var(--hu-primary))] text-[hsl(var(--hu-primary-foreground))] hover:bg-[hsl(var(--hu-primary))]/90 focus:outline-none focus:ring-2 focus:ring-[hsl(var(--hu-ring))]"
        >
          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