Web InnoventixFreeCode

With Info Pagination

Original · free

pagination with results info

byWeb InnoventixReact + Tailwind
pagewithinfopagination
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/page-with-info.json
page-with-info.tsx
"use client";

import {
  useId,
  useMemo,
  useState,
  type ChangeEvent,
  type KeyboardEvent,
} from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";

type TxStatus = "completed" | "pending" | "failed" | "refunded";

type Transaction = {
  id: string;
  reference: string;
  merchant: string;
  method: string;
  date: string;
  amount: number;
  status: TxStatus;
};

const TRANSACTIONS: Transaction[] = [
  { id: "t01", reference: "TXN-9042", merchant: "Figma", method: "Visa •• 4242", date: "Jul 14, 2026", amount: 45.0, status: "completed" },
  { id: "t02", reference: "TXN-9038", merchant: "Vercel", method: "Amex •• 1009", date: "Jul 14, 2026", amount: 20.0, status: "completed" },
  { id: "t03", reference: "TXN-9031", merchant: "Linear", method: "Visa •• 4242", date: "Jul 13, 2026", amount: 96.0, status: "completed" },
  { id: "t04", reference: "TXN-9024", merchant: "Notion", method: "Mastercard •• 8830", date: "Jul 13, 2026", amount: 120.0, status: "pending" },
  { id: "t05", reference: "TXN-9019", merchant: "GitHub", method: "Visa •• 4242", date: "Jul 12, 2026", amount: 21.0, status: "completed" },
  { id: "t06", reference: "TXN-9012", merchant: "OpenAI", method: "Amex •• 1009", date: "Jul 12, 2026", amount: 200.0, status: "completed" },
  { id: "t07", reference: "TXN-9007", merchant: "Anthropic", method: "Visa •• 4242", date: "Jul 11, 2026", amount: 100.0, status: "completed" },
  { id: "t08", reference: "TXN-8998", merchant: "Amazon Web Services", method: "ACH transfer", date: "Jul 11, 2026", amount: 842.16, status: "pending" },
  { id: "t09", reference: "TXN-8991", merchant: "Cloudflare", method: "Mastercard •• 8830", date: "Jul 10, 2026", amount: 25.0, status: "completed" },
  { id: "t10", reference: "TXN-8984", merchant: "Datadog", method: "ACH transfer", date: "Jul 10, 2026", amount: 310.0, status: "completed" },
  { id: "t11", reference: "TXN-8977", merchant: "Sentry", method: "Visa •• 4242", date: "Jul 09, 2026", amount: 29.0, status: "failed" },
  { id: "t12", reference: "TXN-8970", merchant: "Twilio", method: "Amex •• 1009", date: "Jul 09, 2026", amount: 74.55, status: "completed" },
  { id: "t13", reference: "TXN-8963", merchant: "Slack", method: "Visa •• 4242", date: "Jul 08, 2026", amount: 87.5, status: "completed" },
  { id: "t14", reference: "TXN-8956", merchant: "Zoom", method: "Mastercard •• 8830", date: "Jul 08, 2026", amount: 15.99, status: "refunded" },
  { id: "t15", reference: "TXN-8949", merchant: "Adobe Creative Cloud", method: "Visa •• 4242", date: "Jul 07, 2026", amount: 59.99, status: "completed" },
  { id: "t16", reference: "TXN-8942", merchant: "Google Workspace", method: "ACH transfer", date: "Jul 07, 2026", amount: 144.0, status: "completed" },
  { id: "t17", reference: "TXN-8935", merchant: "Stripe", method: "Amex •• 1009", date: "Jul 06, 2026", amount: 12.4, status: "completed" },
  { id: "t18", reference: "TXN-8928", merchant: "DigitalOcean", method: "Visa •• 4242", date: "Jul 06, 2026", amount: 48.0, status: "completed" },
  { id: "t19", reference: "TXN-8921", merchant: "Fly.io", method: "Mastercard •• 8830", date: "Jul 05, 2026", amount: 33.2, status: "pending" },
  { id: "t20", reference: "TXN-8914", merchant: "Retool", method: "Visa •• 4242", date: "Jul 05, 2026", amount: 250.0, status: "completed" },
  { id: "t21", reference: "TXN-8907", merchant: "Airtable", method: "Amex •• 1009", date: "Jul 04, 2026", amount: 60.0, status: "completed" },
  { id: "t22", reference: "TXN-8900", merchant: "Calendly", method: "Visa •• 4242", date: "Jul 04, 2026", amount: 16.0, status: "completed" },
  { id: "t23", reference: "TXN-8893", merchant: "1Password", method: "Mastercard •• 8830", date: "Jul 03, 2026", amount: 95.88, status: "completed" },
  { id: "t24", reference: "TXN-8886", merchant: "Miro", method: "Visa •• 4242", date: "Jul 03, 2026", amount: 40.0, status: "failed" },
  { id: "t25", reference: "TXN-8879", merchant: "Loom", method: "Amex •• 1009", date: "Jul 02, 2026", amount: 150.0, status: "completed" },
  { id: "t26", reference: "TXN-8872", merchant: "Framer", method: "Visa •• 4242", date: "Jul 02, 2026", amount: 30.0, status: "completed" },
  { id: "t27", reference: "TXN-8865", merchant: "Webflow", method: "Mastercard •• 8830", date: "Jul 01, 2026", amount: 39.0, status: "refunded" },
  { id: "t28", reference: "TXN-8858", merchant: "Postmark", method: "Visa •• 4242", date: "Jul 01, 2026", amount: 55.0, status: "completed" },
  { id: "t29", reference: "TXN-8851", merchant: "Raycast", method: "Amex •• 1009", date: "Jun 30, 2026", amount: 96.0, status: "completed" },
  { id: "t30", reference: "TXN-8844", merchant: "Descript", method: "Visa •• 4242", date: "Jun 30, 2026", amount: 288.0, status: "completed" },
  { id: "t31", reference: "TXN-8837", merchant: "Fathom Analytics", method: "Mastercard •• 8830", date: "Jun 29, 2026", amount: 54.0, status: "completed" },
  { id: "t32", reference: "TXN-8830", merchant: "Arc Browser", method: "Visa •• 4242", date: "Jun 29, 2026", amount: 18.0, status: "completed" },
];

const PAGE_SIZE_OPTIONS = [6, 10, 15] as const;

const usd = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});

const STATUS_META: Record<
  TxStatus,
  { label: string; badge: string; dot: string }
> = {
  completed: {
    label: "Completed",
    badge:
      "bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-400/25",
    dot: "bg-emerald-500",
  },
  pending: {
    label: "Pending",
    badge:
      "bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-400/25",
    dot: "bg-amber-500",
  },
  failed: {
    label: "Failed",
    badge:
      "bg-rose-50 text-rose-700 ring-rose-600/20 dark:bg-rose-500/10 dark:text-rose-300 dark:ring-rose-400/25",
    dot: "bg-rose-500",
  },
  refunded: {
    label: "Refunded",
    badge:
      "bg-sky-50 text-sky-700 ring-sky-600/20 dark:bg-sky-500/10 dark:text-sky-300 dark:ring-sky-400/25",
    dot: "bg-sky-500",
  },
};

function buildPageList(
  current: number,
  total: number,
): Array<number | "gap"> {
  if (total <= 7) {
    return Array.from({ length: total }, (_, i) => i + 1);
  }
  const delta = 1;
  const pages: number[] = [];
  for (let i = 1; i <= total; i += 1) {
    if (
      i === 1 ||
      i === total ||
      (i >= current - delta && i <= current + delta)
    ) {
      pages.push(i);
    }
  }
  const out: Array<number | "gap"> = [];
  let prev = 0;
  for (const p of pages) {
    if (prev) {
      if (p - prev === 2) {
        out.push(prev + 1);
      } else if (p - prev > 2) {
        out.push("gap");
      }
    }
    out.push(p);
    prev = p;
  }
  return out;
}

const rowVariants: Variants = {
  hidden: { opacity: 0, y: 8 },
  show: (i: number) => ({
    opacity: 1,
    y: 0,
    transition: {
      delay: Math.min(i, 9) * 0.035,
      duration: 0.3,
      ease: [0.16, 1, 0.3, 1],
    },
  }),
};

function ChevronLeft() {
  return (
    <svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
      <path
        d="M12.5 15 7.5 10l5-5"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ChevronRight() {
  return (
    <svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
      <path
        d="M7.5 5 12.5 10l-5 5"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ChevronsLeft() {
  return (
    <svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
      <path
        d="M10.5 15 5.5 10l5-5M15 15l-5-5 5-5"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ChevronsRight() {
  return (
    <svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
      <path
        d="M9.5 5 14.5 10l-5 5M5 5l5 5-5 5"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function SearchIcon() {
  return (
    <svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
      <circle cx="9" cy="9" r="5.25" stroke="currentColor" strokeWidth="1.7" />
      <path
        d="m13.5 13.5 3 3"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
      />
    </svg>
  );
}

function CloseIcon() {
  return (
    <svg viewBox="0 0 20 20" fill="none" className="h-3.5 w-3.5" aria-hidden="true">
      <path
        d="m6 6 8 8M14 6l-8 8"
        stroke="currentColor"
        strokeWidth="1.8"
        strokeLinecap="round"
      />
    </svg>
  );
}

function ReceiptIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" className="h-5 w-5" aria-hidden="true">
      <path
        d="M6 3.5h12v17l-2.4-1.4-2.4 1.4-2.4-1.4-2.4 1.4L6 20.5v-17Z"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinejoin="round"
      />
      <path
        d="M9 8h6M9 11.5h6M9 15h3.5"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinecap="round"
      />
    </svg>
  );
}

export default function PageWithInfo() {
  const reduce = useReducedMotion();
  const searchId = useId();
  const pageSizeId = useId();

  const [query, setQuery] = useState("");
  const [pageSize, setPageSize] = useState<number>(PAGE_SIZE_OPTIONS[0]);
  const [page, setPage] = useState(1);

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return TRANSACTIONS;
    return TRANSACTIONS.filter((t) => {
      const label = STATUS_META[t.status].label.toLowerCase();
      return (
        t.merchant.toLowerCase().includes(q) ||
        t.reference.toLowerCase().includes(q) ||
        t.method.toLowerCase().includes(q) ||
        label.includes(q)
      );
    });
  }, [query]);

  const total = filtered.length;
  const totalPages = Math.max(1, Math.ceil(total / pageSize));
  const currentPage = Math.min(Math.max(page, 1), totalPages);
  const startIndex = (currentPage - 1) * pageSize;
  const visible = filtered.slice(startIndex, startIndex + pageSize);

  const firstShown = total === 0 ? 0 : startIndex + 1;
  const lastShown = Math.min(startIndex + pageSize, total);
  const pageSum = visible.reduce((acc, t) => acc + t.amount, 0);

  const pageList = buildPageList(currentPage, totalPages);

  function goToPage(next: number) {
    setPage(Math.min(Math.max(next, 1), totalPages));
  }

  function onSearch(e: ChangeEvent<HTMLInputElement>) {
    setQuery(e.target.value);
    setPage(1);
  }

  function onPageSizeChange(e: ChangeEvent<HTMLSelectElement>) {
    setPageSize(Number(e.target.value));
    setPage(1);
  }

  function onNavKeyDown(e: KeyboardEvent<HTMLElement>) {
    if (e.key === "ArrowLeft") {
      e.preventDefault();
      goToPage(currentPage - 1);
    } else if (e.key === "ArrowRight") {
      e.preventDefault();
      goToPage(currentPage + 1);
    } else if (e.key === "Home") {
      e.preventDefault();
      goToPage(1);
    } else if (e.key === "End") {
      e.preventDefault();
      goToPage(totalPages);
    }
  }

  const arrowBtn =
    "inline-flex h-9 w-9 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-white disabled:hover:text-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white dark:disabled:hover:bg-zinc-900 dark:focus-visible:ring-offset-zinc-950";

  return (
    <section className="relative w-full bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-20 dark:bg-zinc-950 dark:text-zinc-100">
      <style>{`
        @keyframes pwinfo-sheen {
          0% { background-position: 0% 50%; }
          100% { background-position: 200% 50%; }
        }
        .pwinfo-accent {
          background-image: linear-gradient(90deg, #6366f1, #0ea5e9, #10b981, #6366f1);
          background-size: 200% 100%;
          animation: pwinfo-sheen 7s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .pwinfo-accent { animation: none; }
        }
      `}</style>

      <div className="mx-auto max-w-5xl">
        <div className="mb-8 flex flex-col gap-2">
          <span className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Billing
          </span>
          <h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
            Transaction history
          </h2>
          <p className="max-w-xl text-sm text-slate-500 dark:text-zinc-400">
            Every charge across your connected subscriptions and infrastructure
            vendors, refreshed hourly.
          </p>
        </div>

        <div className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
          <div className="pwinfo-accent h-1 w-full" aria-hidden="true" />

          {/* Toolbar */}
          <div className="flex flex-col gap-4 border-b border-slate-200 px-5 py-4 sm:flex-row sm:items-center sm:justify-between dark:border-zinc-800">
            <div className="flex items-center gap-3">
              <span className="inline-flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-400">
                <ReceiptIcon />
              </span>
              <div>
                <p className="text-sm font-semibold leading-tight">
                  All transactions
                </p>
                <p className="text-xs text-slate-500 dark:text-zinc-400">
                  {TRANSACTIONS.length} total records
                </p>
              </div>
            </div>

            <div className="relative w-full sm:w-72">
              <label htmlFor={searchId} className="sr-only">
                Search transactions
              </label>
              <span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-zinc-500">
                <SearchIcon />
              </span>
              <input
                id={searchId}
                type="search"
                value={query}
                onChange={onSearch}
                placeholder="Search merchant, ref, status…"
                className="w-full rounded-lg border border-slate-200 bg-slate-50 py-2 pl-9 pr-9 text-sm text-slate-900 placeholder:text-slate-400 transition-colors focus:border-indigo-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:border-indigo-500"
              />
              {query.length > 0 && (
                <button
                  type="button"
                  onClick={() => {
                    setQuery("");
                    setPage(1);
                  }}
                  className="absolute right-2.5 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-slate-200 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
                  aria-label="Clear search"
                >
                  <CloseIcon />
                </button>
              )}
            </div>
          </div>

          {/* Table */}
          <div className="overflow-x-auto">
            <table className="w-full min-w-[560px] border-collapse text-left text-sm">
              <caption className="sr-only">
                List of transactions, paginated
              </caption>
              <thead>
                <tr className="border-b border-slate-200 text-xs uppercase tracking-wider text-slate-500 dark:border-zinc-800 dark:text-zinc-400">
                  <th scope="col" className="px-5 py-3 font-medium">
                    Reference
                  </th>
                  <th scope="col" className="px-5 py-3 font-medium">
                    Merchant
                  </th>
                  <th
                    scope="col"
                    className="hidden px-5 py-3 font-medium md:table-cell"
                  >
                    Date
                  </th>
                  <th scope="col" className="px-5 py-3 text-right font-medium">
                    Amount
                  </th>
                  <th scope="col" className="px-5 py-3 font-medium">
                    Status
                  </th>
                </tr>
              </thead>
              <tbody>
                {visible.length === 0 ? (
                  <tr>
                    <td colSpan={5} className="px-5 py-16 text-center">
                      <p className="text-sm font-medium text-slate-700 dark:text-zinc-200">
                        No transactions match “{query}”.
                      </p>
                      <p className="mt-1 text-sm text-slate-500 dark:text-zinc-400">
                        Try a different merchant name or reference.
                      </p>
                    </td>
                  </tr>
                ) : (
                  visible.map((t, i) => {
                    const meta = STATUS_META[t.status];
                    return (
                      <motion.tr
                        key={`${currentPage}-${pageSize}-${t.id}`}
                        custom={i}
                        variants={reduce ? undefined : rowVariants}
                        initial={reduce ? false : "hidden"}
                        animate={reduce ? undefined : "show"}
                        className="border-b border-slate-100 transition-colors last:border-0 hover:bg-slate-50 dark:border-zinc-800/60 dark:hover:bg-zinc-800/40"
                      >
                        <td className="px-5 py-3.5">
                          <span className="font-mono text-xs text-slate-500 dark:text-zinc-400">
                            {t.reference}
                          </span>
                        </td>
                        <td className="px-5 py-3.5">
                          <span className="block font-medium text-slate-900 dark:text-zinc-100">
                            {t.merchant}
                          </span>
                          <span className="block text-xs text-slate-400 dark:text-zinc-500">
                            {t.method}
                          </span>
                        </td>
                        <td className="hidden px-5 py-3.5 text-slate-500 md:table-cell dark:text-zinc-400">
                          {t.date}
                        </td>
                        <td className="px-5 py-3.5 text-right font-mono tabular-nums text-slate-900 dark:text-zinc-100">
                          {t.status === "refunded" ? "−" : ""}
                          {usd.format(t.amount)}
                        </td>
                        <td className="px-5 py-3.5">
                          <span
                            className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ring-1 ring-inset ${meta.badge}`}
                          >
                            <span
                              className={`h-1.5 w-1.5 rounded-full ${meta.dot}`}
                              aria-hidden="true"
                            />
                            {meta.label}
                          </span>
                        </td>
                      </motion.tr>
                    );
                  })
                )}
              </tbody>
            </table>
          </div>

          {/* Footer: results info + pagination */}
          <div className="flex flex-col gap-4 border-t border-slate-200 px-5 py-4 lg:flex-row lg:items-center lg:justify-between dark:border-zinc-800">
            <div>
              <p
                className="text-sm text-slate-600 dark:text-zinc-300"
                aria-live="polite"
              >
                Showing{" "}
                <span className="font-semibold text-slate-900 dark:text-zinc-100">
                  {firstShown}–{lastShown}
                </span>{" "}
                of{" "}
                <span className="font-semibold text-slate-900 dark:text-zinc-100">
                  {total}
                </span>{" "}
                {total === 1 ? "transaction" : "transactions"}
              </p>
              {total > 0 && (
                <p className="mt-0.5 text-xs text-slate-400 dark:text-zinc-500">
                  {usd.format(pageSum)} on this page
                </p>
              )}
            </div>

            <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:gap-6">
              <div className="flex items-center gap-2">
                <label
                  htmlFor={pageSizeId}
                  className="text-sm text-slate-500 dark:text-zinc-400"
                >
                  Rows
                </label>
                <select
                  id={pageSizeId}
                  value={pageSize}
                  onChange={onPageSizeChange}
                  className="rounded-lg border border-slate-200 bg-white py-1.5 pl-2.5 pr-7 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-offset-zinc-950"
                >
                  {PAGE_SIZE_OPTIONS.map((opt) => (
                    <option key={opt} value={opt}>
                      {opt}
                    </option>
                  ))}
                </select>
              </div>

              <nav
                aria-label="Pagination"
                onKeyDown={onNavKeyDown}
                className="flex items-center gap-1"
              >
                <button
                  type="button"
                  onClick={() => goToPage(1)}
                  disabled={currentPage === 1}
                  className={arrowBtn}
                  aria-label="Go to first page"
                >
                  <ChevronsLeft />
                </button>
                <button
                  type="button"
                  onClick={() => goToPage(currentPage - 1)}
                  disabled={currentPage === 1}
                  className={arrowBtn}
                  aria-label="Go to previous page"
                >
                  <ChevronLeft />
                </button>

                <ul className="flex items-center gap-1">
                  {pageList.map((item, idx) =>
                    item === "gap" ? (
                      <li
                        key={`gap-${idx}`}
                        aria-hidden="true"
                        className="inline-flex h-9 w-9 items-center justify-center text-sm text-slate-400 dark:text-zinc-600"
                      >
                        …
                      </li>
                    ) : (
                      <li key={item}>
                        <button
                          type="button"
                          onClick={() => goToPage(item)}
                          aria-current={
                            item === currentPage ? "page" : undefined
                          }
                          aria-label={
                            item === currentPage
                              ? `Page ${item}, current page`
                              : `Go to page ${item}`
                          }
                          className={
                            "inline-flex h-9 min-w-9 items-center justify-center rounded-lg px-2 text-sm font-medium tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-950 " +
                            (item === currentPage
                              ? "bg-indigo-600 text-white shadow-sm hover:bg-indigo-600"
                              : "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white")
                          }
                        >
                          {item}
                        </button>
                      </li>
                    ),
                  )}
                </ul>

                <button
                  type="button"
                  onClick={() => goToPage(currentPage + 1)}
                  disabled={currentPage === totalPages}
                  className={arrowBtn}
                  aria-label="Go to next page"
                >
                  <ChevronRight />
                </button>
                <button
                  type="button"
                  onClick={() => goToPage(totalPages)}
                  disabled={currentPage === totalPages}
                  className={arrowBtn}
                  aria-label="Go to last page"
                >
                  <ChevronsRight />
                </button>
              </nav>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →