diff --git a/packages/components/pagination/README.md b/packages/components/pagination/README.md
new file mode 100644
index 000000000..e0966aad5
--- /dev/null
+++ b/packages/components/pagination/README.md
@@ -0,0 +1,24 @@
+# @nextui-org/pagination
+
+A Quick description of the component
+
+> This is an internal utility, not intended for public usage.
+
+## Installation
+
+```sh
+yarn add @nextui-org/pagination
+# or
+npm i @nextui-org/pagination
+```
+
+## Contribution
+
+Yes please! See the
+[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
+for details.
+
+## Licence
+
+This project is licensed under the terms of the
+[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).
\ No newline at end of file
diff --git a/packages/components/pagination/__tests__/pagination.test.tsx b/packages/components/pagination/__tests__/pagination.test.tsx
new file mode 100644
index 000000000..9d73d5b92
--- /dev/null
+++ b/packages/components/pagination/__tests__/pagination.test.tsx
@@ -0,0 +1,63 @@
+import * as React from "react";
+import {render} from "@testing-library/react";
+
+import {Pagination} from "../src";
+
+describe("Pagination", () => {
+ it("should render correctly", () => {
+ const wrapper = render();
+
+ expect(() => wrapper.unmount()).not.toThrow();
+ });
+
+ it("ref should be forwarded", () => {
+ const ref = React.createRef();
+
+ render();
+ expect(ref.current).not.toBeNull();
+ });
+
+ it("should render correctly with controls", () => {
+ const wrapper = render();
+
+ const nextButton = wrapper.getByLabelText("next page button");
+ const prevButton = wrapper.getByLabelText("previous page button");
+
+ expect(nextButton).not.toBeNull();
+ expect(prevButton).not.toBeNull();
+ });
+
+ it("should render correctly with no controls", () => {
+ const wrapper = render();
+
+ const nextButton = wrapper.queryByLabelText("next page button");
+ const prevButton = wrapper.queryByLabelText("previous page button");
+
+ expect(nextButton).toBeNull();
+ expect(prevButton).toBeNull();
+ });
+
+ it("should show dots when total is greater than 10", () => {
+ const wrapper = render();
+
+ const dots = wrapper.queryAllByLabelText("dots element");
+
+ expect(dots).toHaveLength(1);
+ });
+
+ it("should show 2 dots when total is greater than 10 and the initialPage is in de middle", () => {
+ const wrapper = render();
+
+ const dots = wrapper.queryAllByLabelText("dots element");
+
+ expect(dots).toHaveLength(2);
+ });
+
+ it("the pagination highlight should have a aria-hidden attribute", () => {
+ const {container} = render();
+
+ const highlight = container.querySelector(".nextui-pagination-highlight");
+
+ expect(highlight).toHaveAttribute("aria-hidden", "true");
+ });
+});
diff --git a/packages/components/pagination/clean-package.config.json b/packages/components/pagination/clean-package.config.json
new file mode 100644
index 000000000..ea9967219
--- /dev/null
+++ b/packages/components/pagination/clean-package.config.json
@@ -0,0 +1,3 @@
+{ "replace": { "main": "dist/index.cjs.js", "module": "dist/index.esm.js",
+"types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.esm.js",
+"require": "./dist/index.cjs.js" }, "./package.json": "./package.json" } } }
\ No newline at end of file
diff --git a/packages/components/pagination/package.json b/packages/components/pagination/package.json
new file mode 100644
index 000000000..cfb8f2e0b
--- /dev/null
+++ b/packages/components/pagination/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@nextui-org/pagination",
+ "version": "1.0.0-beta.11",
+ "description": "The Pagination component allows you to display active page and navigate between multiple pages.",
+ "keywords": [
+ "pagination"
+ ],
+ "author": "Junior Garcia ",
+ "homepage": "https://nextui.org",
+ "license": "MIT",
+ "main": "src/index.ts",
+ "sideEffects": false,
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/nextui-org/nextui.git",
+ "directory": "packages/components/pagination"
+ },
+ "bugs": {
+ "url": "https://github.com/nextui-org/nextui/issues"
+ },
+ "scripts": {
+ "build": "tsup src/index.ts --format=esm,cjs --dts",
+ "dev": "yarn build:fast -- --watch",
+ "clean": "rimraf dist .turbo",
+ "typecheck": "tsc --noEmit",
+ "build:fast": "tsup src/index.ts --format=esm,cjs",
+ "prepack": "clean-package",
+ "postpack": "clean-package restore"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "dependencies": {
+ "@nextui-org/system": "workspace:*",
+ "@nextui-org/shared-utils": "workspace:*",
+ "@nextui-org/shared-css": "workspace:*",
+ "@nextui-org/dom-utils": "workspace:*",
+ "@nextui-org/use-pagination": "workspace:*",
+ "@nextui-org/grid": "workspace:*",
+ "@react-aria/utils": "^3.14.0"
+ },
+ "devDependencies": {
+ "clean-package": "2.1.1",
+ "react": "^17.0.2"
+ }
+}
diff --git a/packages/components/pagination/src/index.ts b/packages/components/pagination/src/index.ts
new file mode 100644
index 000000000..67f749023
--- /dev/null
+++ b/packages/components/pagination/src/index.ts
@@ -0,0 +1,5 @@
+// export types
+export type {PaginationProps} from "./pagination";
+
+// export component
+export {default as Pagination} from "./pagination";
diff --git a/packages/components/pagination/src/pagination-ellipsis.tsx b/packages/components/pagination/src/pagination-ellipsis.tsx
new file mode 100644
index 000000000..028ceb3c7
--- /dev/null
+++ b/packages/components/pagination/src/pagination-ellipsis.tsx
@@ -0,0 +1,81 @@
+import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
+import {useDOMRef} from "@nextui-org/dom-utils";
+import {useState, MouseEvent} from "react";
+import {clsx, __DEV__} from "@nextui-org/shared-utils";
+
+import PaginationItem from "./pagination-item";
+import {StyledPaginationEllipsis} from "./pagination.styles";
+
+export interface PaginationEllipsisProps extends HTMLNextUIProps<"svg"> {
+ value?: string | number;
+ isBefore?: boolean;
+ onlyDots?: boolean;
+ animated?: boolean;
+ bordered?: boolean;
+ onClick?: (e: MouseEvent) => void;
+}
+
+const PaginationEllipsis = forwardRef((props, ref) => {
+ const [showMore, setShowMore] = useState(false);
+
+ const {className, value, animated, bordered, onlyDots, isBefore, onClick, ...otherProps} = props;
+
+ const domRef = useDOMRef(ref);
+
+ return (
+ onClick?.(e)}
+ onMouseEnter={() => setShowMore(true)}
+ onMouseLeave={() => setShowMore(false)}
+ >
+ {showMore ? (
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+ );
+});
+
+if (__DEV__) {
+ PaginationEllipsis.displayName = "NextUI.PaginationEllipsis";
+}
+
+PaginationEllipsis.toString = () => ".nextui-pagination-ellipsis";
+
+export default PaginationEllipsis;
diff --git a/packages/components/pagination/src/pagination-highlight.tsx b/packages/components/pagination/src/pagination-highlight.tsx
new file mode 100644
index 000000000..809255fea
--- /dev/null
+++ b/packages/components/pagination/src/pagination-highlight.tsx
@@ -0,0 +1,67 @@
+import {useState, useEffect, useMemo} from "react";
+import {mergeProps} from "@react-aria/utils";
+import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
+import {useDOMRef} from "@nextui-org/dom-utils";
+import {clsx, __DEV__} from "@nextui-org/shared-utils";
+
+import {StyledPaginationHighlight} from "./pagination.styles";
+
+export interface PaginationHighlightProps extends HTMLNextUIProps<"div"> {
+ active: number;
+ rounded?: boolean;
+ animated?: boolean;
+ noMargin?: boolean;
+ shadow?: boolean;
+}
+
+const PaginationHighlight = forwardRef((props, ref) => {
+ const {className, css, active, shadow, noMargin, rounded, ...otherProps} = props;
+ const [selfActive, setSelfActive] = useState(active);
+ const [moveClassName, setMoveClassName] = useState("");
+
+ const domRef = useDOMRef(ref);
+
+ useEffect(() => {
+ if (active !== selfActive) {
+ setSelfActive(active);
+ setMoveClassName(`nextui-pagination-highlight--moving`);
+ const timer = setTimeout(() => {
+ setMoveClassName("");
+ clearTimeout(timer);
+ }, 350);
+ }
+ }, [active]);
+
+ const leftValue = useMemo(
+ () =>
+ noMargin
+ ? `var(--nextui--paginationSize) * ${selfActive}`
+ : `var(--nextui--paginationSize) * ${selfActive} + ${selfActive * 4 + 2}px`,
+ [selfActive, noMargin],
+ );
+
+ return (
+
+ );
+});
+
+if (__DEV__) {
+ PaginationHighlight.displayName = "NextUI.PaginationHighlight";
+}
+
+PaginationHighlight.toString = () => ".nextui-pagination-highlight";
+
+export default PaginationHighlight;
diff --git a/packages/components/pagination/src/pagination-icon.tsx b/packages/components/pagination/src/pagination-icon.tsx
new file mode 100644
index 000000000..eb2baf7b6
--- /dev/null
+++ b/packages/components/pagination/src/pagination-icon.tsx
@@ -0,0 +1,62 @@
+import {MouseEvent} from "react";
+import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
+import {useDOMRef} from "@nextui-org/dom-utils";
+import {clsx, __DEV__} from "@nextui-org/shared-utils";
+
+import PaginationItem from "./pagination-item";
+import {StyledPaginationIcon} from "./pagination.styles";
+
+export interface PaginationIconProps extends HTMLNextUIProps<"svg"> {
+ isPrev?: boolean;
+ disabled?: boolean;
+ onlyDots?: boolean;
+ animated?: boolean;
+ bordered?: boolean;
+ onClick?: (e: MouseEvent) => void;
+}
+
+const PaginationIcon = forwardRef((props, ref) => {
+ const {className, isPrev, disabled, onlyDots, animated, bordered, onClick, ...otherProps} = props;
+
+ const domRef = useDOMRef(ref);
+
+ return (
+ "}
+ onClick={(e) => onClick?.(e)}
+ >
+
+
+
+
+ );
+});
+
+if (__DEV__) {
+ PaginationIcon.displayName = "NextUI.PaginationIcon";
+}
+
+PaginationIcon.toString = () => ".nextui-pagination-icon";
+
+export default PaginationIcon;
diff --git a/packages/components/pagination/src/pagination-item.tsx b/packages/components/pagination/src/pagination-item.tsx
new file mode 100644
index 000000000..98c16ffaf
--- /dev/null
+++ b/packages/components/pagination/src/pagination-item.tsx
@@ -0,0 +1,93 @@
+import {useMemo, MouseEvent} from "react";
+import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
+import {useDOMRef} from "@nextui-org/dom-utils";
+import {clsx, __DEV__} from "@nextui-org/shared-utils";
+import {DOTS} from "@nextui-org/use-pagination";
+
+import {StyledPaginationItem, StyledPaginationItemContent} from "./pagination.styles";
+
+export interface PaginationItemProps extends HTMLNextUIProps<"button"> {
+ active?: boolean;
+ value?: string | number;
+ onlyDots?: boolean;
+ disabled?: boolean;
+ bordered?: boolean;
+ animated?: boolean;
+ preserveContent?: boolean;
+ onClick?: (e: MouseEvent) => void;
+}
+
+const getItemAriaLabel = (page?: string | number) => {
+ if (!page) return;
+ switch (page) {
+ case DOTS:
+ return "dots element";
+ case "<":
+ return "previous page button";
+ case ">":
+ return "next page button";
+ case "first":
+ return "first page button";
+ case "last":
+ return "last page button";
+ default:
+ return `${page} item`;
+ }
+};
+
+const PaginationItem = forwardRef((props, ref) => {
+ const {
+ children,
+ className,
+ active,
+ value,
+ animated,
+ bordered,
+ disabled,
+ onlyDots,
+ preserveContent = false,
+ onClick,
+ ...otherProps
+ } = props;
+
+ const domRef = useDOMRef(ref);
+
+ const ariaLabel = useMemo(
+ () => (active ? `${getItemAriaLabel(value)} active` : getItemAriaLabel(value)),
+ [value, active],
+ );
+
+ const clickHandler = (event: React.MouseEvent) => {
+ if (disabled) return;
+ onClick?.(event);
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+});
+
+if (__DEV__) {
+ PaginationItem.displayName = "NextUI.PaginationItem";
+}
+
+PaginationItem.toString = () => ".nextui-pagination-item";
+
+export default PaginationItem;
diff --git a/packages/components/pagination/src/pagination.animations.ts b/packages/components/pagination/src/pagination.animations.ts
new file mode 100644
index 000000000..0842f162c
--- /dev/null
+++ b/packages/components/pagination/src/pagination.animations.ts
@@ -0,0 +1,13 @@
+import {keyframes} from "@nextui-org/system";
+
+export const scale = keyframes({
+ "0%": {
+ transform: "scale(1)",
+ },
+ "60%": {
+ transform: "scale($$paginationScaleTransform)",
+ },
+ "100%": {
+ transform: "scale(1)",
+ },
+});
diff --git a/packages/components/pagination/src/pagination.styles.ts b/packages/components/pagination/src/pagination.styles.ts
new file mode 100644
index 000000000..8398edf08
--- /dev/null
+++ b/packages/components/pagination/src/pagination.styles.ts
@@ -0,0 +1,366 @@
+import {styled} from "@nextui-org/system";
+import {sharedFocus} from "@nextui-org/shared-css";
+
+import {scale} from "./pagination.animations";
+
+export const StyledPaginationEllipsis = styled("svg", {
+ color: "currentColor",
+ stroke: "currentColor",
+ variants: {
+ isEllipsis: {
+ true: {
+ transform: "0deg",
+ },
+ },
+ isBefore: {
+ true: {},
+ },
+ },
+ compoundVariants: [
+ {
+ // isEllipsis && isBefore
+ isEllipsis: true,
+ isBefore: true,
+ css: {
+ transform: "rotate(180deg)",
+ },
+ },
+ ],
+});
+
+export const StyledPaginationIcon = styled("svg", {
+ transform: "rotate(180deg)",
+ variants: {
+ isPrev: {
+ true: {
+ transform: "rotate(0deg)",
+ },
+ },
+ },
+});
+
+export const StyledPaginationItemContent = styled("span", {
+ position: "relative",
+ display: "inline-flex",
+ alignItems: "center",
+ top: 0,
+ left: 0,
+ zIndex: "$2",
+});
+
+export const StyledPaginationItem = styled(
+ "button",
+ {
+ border: "none",
+ position: "relative",
+ display: "inline-flex",
+ margin: "0 $$paginationItemMargin",
+ ai: "center",
+ jc: "center",
+ padding: 0,
+ boxSizing: "border-box",
+ tt: "capitalize",
+ us: "none",
+ whiteSpace: "nowrap",
+ ta: "center",
+ verticalAlign: "middle",
+ bs: "none",
+ outline: "none",
+ height: "$$paginationSize",
+ minWidth: "$$paginationSize",
+ fs: "inherit",
+ cursor: "pointer",
+ br: "$$paginationItemRadius",
+ color: "$text",
+ bg: "$accents0",
+ "@motion": {
+ transition: "none",
+ },
+ "&:hover": {
+ bg: "$accents1",
+ },
+ [`& ${StyledPaginationIcon}`]: {
+ size: "$$paginationFontSize",
+ },
+ [`& ${StyledPaginationEllipsis}`]: {
+ size: "$$paginationFontSize",
+ },
+ variants: {
+ active: {
+ true: {
+ fontWeight: "$bold",
+ cursor: "default",
+ boxShadow: "$sm",
+ [`& ${StyledPaginationItemContent}`]: {
+ color: "$white",
+ },
+ },
+ },
+ disabled: {
+ true: {
+ color: "$accents5",
+ cursor: "not-allowed",
+ },
+ },
+ bordered: {
+ true: {
+ bg: "transparent",
+ border: "$$paginationItemBorderWeight solid $accents2",
+ },
+ },
+ onlyDots: {
+ true: {},
+ },
+ preserveContent: {
+ true: {},
+ },
+ animated: {
+ true: {
+ transition: "transform 0.25s ease 0s, background 0.25s ease 0s, box-shadow 0.25s ease 0s",
+ },
+ false: {
+ transition: "none",
+ },
+ },
+ },
+ compoundVariants: [
+ // onlyDots && !preserveContent
+ {
+ onlyDots: true,
+ preserveContent: false,
+ css: {
+ [`& ${StyledPaginationItemContent}`]: {
+ display: "none",
+ },
+ },
+ },
+ // animated && !disabled && !active
+ {
+ animated: true,
+ disabled: false,
+ active: false,
+ css: {
+ "&:active": {
+ transform: "scale($$paginationScaleTransform)",
+ fs: "calc($$paginationFontSize * 0.9)",
+ },
+ },
+ },
+ ],
+ },
+ sharedFocus,
+);
+
+export const StyledPaginationHighlight = styled("div", {
+ position: "absolute",
+ contain: "strict",
+ top: "0px",
+ zIndex: "$1",
+ bg: "$$paginationColor",
+ br: "$$paginationItemRadius",
+ height: "$$paginationSize",
+ minWidth: "$$paginationSize",
+ animationName: `${scale}`,
+ animationDirection: "normal",
+ "&.nextui-pagination-highlight--moving": {
+ transform: "scale($$paginationScaleTransform)",
+ },
+ "@motion": {
+ transition: "none",
+ "&.nextui-pagination-highlight--moving": {
+ transform: "scale(1)",
+ },
+ },
+ variants: {
+ animated: {
+ true: {
+ animationDuration: "350ms",
+ animationTimingFunction: "ease",
+ transition: "left 350ms ease 0s, transform 300ms ease 0s",
+ },
+ false: {
+ animationDuration: "none",
+ animationTimingFunction: "none",
+ transition: "none",
+ "&.nextui-pagination-highlight--moving": {
+ transform: "scale(1)",
+ },
+ },
+ },
+ noMargin: {
+ true: {
+ br: "$squared",
+ },
+ },
+ rounded: {
+ true: {},
+ },
+ shadow: {
+ true: {
+ normalShadowVar: "$$paginationShadowColor",
+ },
+ },
+ },
+ compoundVariants: [
+ {
+ // rounded && noMargin
+ rounded: true,
+ noMargin: true,
+ css: {
+ br: "$pill",
+ },
+ },
+ ],
+});
+
+export const StyledPagination = styled("nav", {
+ m: 0,
+ p: 0,
+ d: "inline-flex",
+ position: "relative",
+ fontVariant: "tabular-nums",
+ fontFeatureSettings: "tnum",
+ variants: {
+ color: {
+ default: {
+ $$paginationColor: "$colors$primary",
+ $$paginationShadowColor: "$colors$primaryShadow",
+ },
+ primary: {
+ $$paginationColor: "$colors$primary",
+ $$paginationShadowColor: "$colors$primaryShadow",
+ },
+ secondary: {
+ $$paginationColor: "$colors$secondary",
+ $$paginationShadowColor: "$colors$secondaryShadow",
+ },
+ success: {
+ $$paginationColor: "$colors$success",
+ $$paginationShadowColor: "$colors$successShadow",
+ },
+ warning: {
+ $$paginationColor: "$colors$warning",
+ $$paginationShadowColor: "$colors$warningShadow",
+ },
+ error: {
+ $$paginationColor: "$colors$error",
+ $$paginationShadowColor: "$colors$errorShadow",
+ },
+ gradient: {
+ $$paginationColor: "$colors$gradient",
+ $$paginationShadowColor: "$colors$primaryShadow",
+ },
+ },
+ size: {
+ xs: {
+ $$paginationWidth: "$space$10",
+ $$paginationFontSize: "$space$5",
+ fs: "$$paginationFontSize",
+ },
+ sm: {
+ $$paginationWidth: "$space$12",
+ $$paginationFontSize: "$space$6",
+ fs: "$$paginationFontSize",
+ },
+ md: {
+ $$paginationWidth: "$space$13",
+ $$paginationFontSize: "$space$7",
+ fs: "$$paginationFontSize",
+ },
+ lg: {
+ $$paginationWidth: "$space$14",
+ $$paginationFontSize: "$space$8",
+ fs: "$$paginationFontSize",
+ },
+ xl: {
+ $$paginationWidth: "$space$15",
+ $$paginationFontSize: "$space$9",
+ fs: "$$paginationFontSize",
+ },
+ },
+ borderWeight: {
+ light: {
+ $$paginationItemBorderWeight: "$borderWeights$light",
+ },
+ normal: {
+ $$paginationItemBorderWeight: "$borderWeights$normal",
+ },
+ bold: {
+ $$paginationItemBorderWeight: "$borderWeights$bold",
+ },
+ extrabold: {
+ $$paginationItemBorderWeight: "$borderWeights$extrabold",
+ },
+ black: {
+ $$paginationItemBorderWeight: "$borderWeights$black",
+ },
+ },
+ bordered: {
+ true: {},
+ },
+ onlyDots: {
+ true: {
+ $$paginationSize: "calc($$paginationWidth / 2)",
+ $$paginationItemRadius: "$radii$pill",
+ $$paginationScaleTransform: 1.05,
+ },
+ false: {
+ $$paginationSize: "$$paginationWidth",
+ $$paginationScaleTransform: 1.1,
+ },
+ },
+ rounded: {
+ true: {
+ $$paginationItemRadius: "$radii$pill",
+ },
+ false: {
+ $$paginationItemRadius: "$radii$squared",
+ },
+ },
+ noMargin: {
+ true: {
+ $$paginationItemRadius: "0px",
+ $$paginationItemMargin: "0",
+ [`& ${StyledPaginationItem}:first-of-type`]: {
+ btlr: "$squared",
+ bblr: "$squared",
+ },
+ [`& ${StyledPaginationItem}:last-of-type`]: {
+ btrr: "$squared",
+ bbrr: "$squared",
+ },
+ },
+ false: {
+ $$paginationItemMargin: "$space$1",
+ },
+ },
+ },
+ compoundVariants: [
+ {
+ // bordered && noMargin
+ bordered: true,
+ noMargin: true,
+ css: {
+ [`& ${StyledPaginationItem}:not(:last-child)`]: {
+ borderRight: 0,
+ },
+ },
+ },
+ {
+ // noMargin && rounded
+ noMargin: true,
+ rounded: true,
+ css: {
+ $$paginationItemRadius: "0px",
+ },
+ },
+ {
+ // !rounded && noMargin
+ rounded: false,
+ noMargin: true,
+ css: {
+ $$paginationItemRadius: "0px",
+ },
+ },
+ ],
+});
diff --git a/packages/components/pagination/src/pagination.tsx b/packages/components/pagination/src/pagination.tsx
new file mode 100644
index 000000000..874927a91
--- /dev/null
+++ b/packages/components/pagination/src/pagination.tsx
@@ -0,0 +1,127 @@
+import type {PaginationItemParam} from "@nextui-org/use-pagination";
+
+import {useCallback} from "react";
+import {forwardRef} from "@nextui-org/system";
+import {useDOMRef} from "@nextui-org/dom-utils";
+import {DOTS} from "@nextui-org/use-pagination";
+import {clsx, __DEV__} from "@nextui-org/shared-utils";
+
+import PaginationItem from "./pagination-item";
+import PaginationEllipsis from "./pagination-ellipsis";
+import PaginationHighlight from "./pagination-highlight";
+import PaginationIcon from "./pagination-icon";
+import {StyledPagination} from "./pagination.styles";
+import {UsePaginationProps, usePagination} from "./use-pagination";
+
+export interface PaginationProps extends UsePaginationProps {}
+
+const Pagination = forwardRef((props, ref) => {
+ const {
+ className,
+ controls,
+ animated,
+ rounded,
+ bordered,
+ shadow,
+ onlyDots,
+ dotsJump,
+ noMargin,
+ loop,
+ total,
+ range,
+ active,
+ setPage,
+ onPrevious,
+ onNext,
+ ...otherProps
+ } = usePagination(props);
+
+ const domRef = useDOMRef(ref);
+
+ const renderItem = useCallback(
+ (value: PaginationItemParam, index: number) => {
+ if (value === DOTS) {
+ const isBefore = index < range.indexOf(active);
+
+ return (
+
+ isBefore
+ ? setPage(active - dotsJump >= 1 ? active - dotsJump : 1)
+ : setPage(active + dotsJump <= total ? active + dotsJump : total)
+ }
+ />
+ );
+ }
+
+ return (
+ value !== active && setPage(value)}
+ >
+ {value}
+
+ );
+ },
+ [total, onlyDots, active, bordered, animated],
+ );
+
+ return (
+
+ {controls && (
+
+ )}
+
+ {range.map(renderItem)}
+ {controls && (
+
+ )}
+
+ );
+});
+
+if (__DEV__) {
+ Pagination.displayName = "NextUI.Pagination";
+}
+
+Pagination.toString = () => ".nextui-pagination";
+
+export default Pagination;
diff --git a/packages/components/pagination/src/use-pagination.ts b/packages/components/pagination/src/use-pagination.ts
new file mode 100644
index 000000000..e72a3d5de
--- /dev/null
+++ b/packages/components/pagination/src/use-pagination.ts
@@ -0,0 +1,144 @@
+import type {NormalColors, NormalSizes, NormalWeights} from "@nextui-org/shared-utils";
+import type {HTMLNextUIProps} from "@nextui-org/system";
+
+import {
+ usePagination as useBasePagination,
+ UsePaginationProps as UseBasePaginationProps,
+} from "@nextui-org/use-pagination";
+
+export interface UsePaginationProps extends HTMLNextUIProps<"nav", UseBasePaginationProps> {
+ /**
+ * The pagination color.
+ * @default "default"
+ */
+ color?: NormalColors;
+ /**
+ * The pagination size.
+ * @default "md"
+ */
+ size?: NormalSizes;
+ /**
+ * The border weight of the bordered pagination.
+ * @default "normal"
+ */
+ borderWeight?: NormalWeights;
+ /**
+ * Show only dots as pagination elements
+ * @default false
+ */
+ onlyDots?: boolean;
+ /**
+ * Number of pages that are added or subtracted on the '...' button.
+ * @default 5
+ */
+ dotsJump?: number;
+ /**
+ * Whether the pagination is bordered.
+ * @default false
+ */
+ bordered?: boolean;
+ /**
+ * Whether the pagination is rounded.
+ * @default false
+ */
+ rounded?: boolean;
+ /**
+ * Whether to show a shadow effect.
+ * @default false
+ */
+ shadow?: boolean;
+ /**
+ * Whether the pagination should have animations.
+ * @default true
+ */
+ animated?: boolean;
+ /**
+ * Non disable next/previous controls
+ * @default false
+ */
+ loop?: boolean;
+ /**
+ * Whether the pagination should have a margin.
+ * @default false
+ */
+ noMargin?: boolean;
+ /**
+ * Whether the pagination should display controls (left/right arrows).
+ * @default true
+ */
+ controls?: boolean;
+}
+
+export function usePagination(props: UsePaginationProps) {
+ const {
+ color = "default",
+ size = "md",
+ borderWeight = "normal",
+ rounded = false,
+ noMargin = false,
+ bordered = false,
+ animated = true,
+ shadow = false,
+ onlyDots = false,
+ controls = true,
+ dotsJump = 5,
+ loop = false,
+ total = 1,
+ initialPage,
+ page,
+ siblings,
+ boundaries,
+ onChange,
+
+ ...otherProps
+ } = props;
+
+ const {range, active, setPage, previous, next, first, last} = useBasePagination({
+ page,
+ initialPage,
+ siblings: onlyDots ? 10 : siblings,
+ boundaries: onlyDots ? 10 : boundaries,
+ total,
+ onChange,
+ });
+
+ const onNext = () => {
+ if (loop && active === total) {
+ return first();
+ }
+
+ return next();
+ };
+
+ const onPrevious = () => {
+ if (loop && active === 1) {
+ return last();
+ }
+
+ return previous();
+ };
+
+ return {
+ color,
+ size,
+ bordered,
+ shadow,
+ onlyDots,
+ controls,
+ dotsJump,
+ animated,
+ noMargin,
+ borderWeight,
+ loop,
+ total,
+ active,
+ rounded,
+ range,
+ setPage,
+ onNext,
+ onPrevious,
+ ...otherProps,
+ };
+}
+
+export type UsePaginationReturn = ReturnType;
diff --git a/packages/components/pagination/stories/pagination.stories.tsx b/packages/components/pagination/stories/pagination.stories.tsx
new file mode 100644
index 000000000..71d8d54b5
--- /dev/null
+++ b/packages/components/pagination/stories/pagination.stories.tsx
@@ -0,0 +1,161 @@
+import React from "react";
+import {Meta} from "@storybook/react";
+import {Grid} from "@nextui-org/grid";
+
+import {Pagination} from "../src";
+
+export default {
+ title: "Navigation/Pagination",
+ component: Pagination,
+} as Meta;
+
+export const Default = () => ;
+
+export const Colors = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export const Sizes = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export const Rounded = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export const Bordered = () => (
+ <>
+
+
+
+
+
+
+ >
+);
+
+export const Shadow = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export const OnlyDots = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export const Loop = () => (
+ <>
+
+
+
+ >
+);
+
+export const NoMargin = () => (
+ <>
+
+
+
+ >
+);
+
+export const NoControls = () => (
+ <>
+
+
+
+ >
+);
+
+export const NoAnimated = () => (
+ <>
+
+
+
+ >
+);
diff --git a/packages/components/pagination/tsconfig.json b/packages/components/pagination/tsconfig.json
new file mode 100644
index 000000000..80a8d5de0
--- /dev/null
+++ b/packages/components/pagination/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "paths": {
+ "@stitches/react": ["../../../node_modules/@stitches/react"]
+ }
+ },
+ "include": ["src", "index.ts"]
+}
diff --git a/packages/components/pagination/tsup.config.ts b/packages/components/pagination/tsup.config.ts
new file mode 100644
index 000000000..8bcd44aa9
--- /dev/null
+++ b/packages/components/pagination/tsup.config.ts
@@ -0,0 +1,13 @@
+import {defineConfig} from "tsup";
+import {findUpSync} from "find-up";
+
+export default defineConfig({
+ clean: true,
+ minify: false,
+ treeshake: true,
+ format: ["cjs", "esm"],
+ outExtension(ctx) {
+ return {js: `.${ctx.format}.js`};
+ },
+ inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined,
+});
diff --git a/packages/core/react/package.json b/packages/core/react/package.json
index 15c31fc7f..796f80583 100644
--- a/packages/core/react/package.json
+++ b/packages/core/react/package.json
@@ -58,7 +58,8 @@
"@nextui-org/snippet": "workspace:*",
"@nextui-org/spacer": "workspace:*",
"@nextui-org/text": "workspace:*",
- "@nextui-org/user": "workspace:*"
+ "@nextui-org/user": "workspace:*",
+ "@nextui-org/pagination": "workspace:*"
},
"peerDependencies": {
"react": ">=16.8.0",
diff --git a/packages/core/react/src/index.ts b/packages/core/react/src/index.ts
index afa9eb031..3380dc528 100644
--- a/packages/core/react/src/index.ts
+++ b/packages/core/react/src/index.ts
@@ -9,3 +9,4 @@ export * from "@nextui-org/grid";
export * from "@nextui-org/text";
export * from "@nextui-org/link";
export * from "@nextui-org/code";
+export * from "@nextui-org/pagination";
diff --git a/packages/hooks/use-pagination/README.md b/packages/hooks/use-pagination/README.md
new file mode 100644
index 000000000..ae918603c
--- /dev/null
+++ b/packages/hooks/use-pagination/README.md
@@ -0,0 +1,24 @@
+# @nextui-org/use-pagination
+
+A Quick description of the component
+
+> This is an internal utility, not intended for public usage.
+
+## Installation
+
+```sh
+yarn add @nextui-org/use-pagination
+# or
+npm i @nextui-org/use-pagination
+```
+
+## Contribution
+
+Yes please! See the
+[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
+for details.
+
+## Licence
+
+This project is licensed under the terms of the
+[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).
\ No newline at end of file
diff --git a/packages/hooks/use-pagination/__tests__/use-pagination.test.tsx b/packages/hooks/use-pagination/__tests__/use-pagination.test.tsx
new file mode 100644
index 000000000..064497429
--- /dev/null
+++ b/packages/hooks/use-pagination/__tests__/use-pagination.test.tsx
@@ -0,0 +1,102 @@
+import {renderHook, act} from "@testing-library/react-hooks";
+
+import {usePagination} from "../src";
+
+describe("usePagination", () => {
+ it("should work correctly", () => {
+ const {result} = renderHook(() => usePagination({total: 10}));
+
+ act(() => result.current.setPage(5));
+ expect(result.current.active).toBe(5);
+ });
+
+ it("should return correct initial state", () => {
+ const {result} = renderHook(() => usePagination({total: 10}));
+
+ expect(result.current.range).toStrictEqual([1, 2, 3, 4, 5, "dots", 10]);
+ expect(result.current.active).toBe(1);
+ });
+
+ it("should not change range length between page changes", () => {
+ const {result} = renderHook(() => usePagination({total: 10}));
+
+ [...new Array(10)].forEach(() => {
+ expect(result.current.range.length).toBe(7);
+ act(() => result.current.next());
+ });
+ });
+
+ it("should return the correct initial state with custom parameters", () => {
+ const {result} = renderHook(() =>
+ usePagination({
+ total: 20,
+ siblings: 2,
+ boundaries: 2,
+ initialPage: 7,
+ }),
+ );
+
+ expect(result.current.range).toStrictEqual([1, 2, "dots", 5, 6, 7, 8, 9, "dots", 19, 20]);
+ expect(result.current.active).toBe(7);
+ });
+
+ it("should work correctly with custom siblings", () => {
+ const {result} = renderHook(() =>
+ usePagination({
+ total: 20,
+ page: 7,
+ siblings: 2,
+ }),
+ );
+
+ expect(result.current.range).toStrictEqual([1, "dots", 5, 6, 7, 8, 9, "dots", 20]);
+ expect(result.current.active).toBe(7);
+ });
+
+ it("should work correctly without siblings", () => {
+ const {result} = renderHook(() =>
+ usePagination({
+ total: 20,
+ page: 7,
+ siblings: 0,
+ }),
+ );
+
+ expect(result.current.range).toStrictEqual([1, "dots", 7, "dots", 20]);
+ expect(result.current.active).toBe(7);
+ });
+
+ it("should work correctly with custom boundaries", () => {
+ const {result} = renderHook(() =>
+ usePagination({
+ total: 20,
+ page: 7,
+ boundaries: 2,
+ }),
+ );
+
+ expect(result.current.range).toStrictEqual([1, 2, "dots", 6, 7, 8, "dots", 19, 20]);
+ expect(result.current.active).toBe(7);
+ });
+
+ it("should work correctly without boundaries", () => {
+ const {result} = renderHook(() =>
+ usePagination({
+ total: 20,
+ page: 7,
+ boundaries: 0,
+ }),
+ );
+
+ expect(result.current.range).toStrictEqual(["dots", 6, 7, 8, "dots"]);
+ expect(result.current.active).toBe(7);
+ });
+
+ it("should call onChange function correctly", () => {
+ const onChange = jest.fn();
+ const {result} = renderHook(() => usePagination({total: 10, onChange}));
+
+ act(() => result.current.setPage(5));
+ expect(onChange).toBeCalledWith(5);
+ });
+});
diff --git a/packages/hooks/use-pagination/clean-package.config.json b/packages/hooks/use-pagination/clean-package.config.json
new file mode 100644
index 000000000..ea9967219
--- /dev/null
+++ b/packages/hooks/use-pagination/clean-package.config.json
@@ -0,0 +1,3 @@
+{ "replace": { "main": "dist/index.cjs.js", "module": "dist/index.esm.js",
+"types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.esm.js",
+"require": "./dist/index.cjs.js" }, "./package.json": "./package.json" } } }
\ No newline at end of file
diff --git a/packages/hooks/use-pagination/package.json b/packages/hooks/use-pagination/package.json
new file mode 100644
index 000000000..4bf88d5e3
--- /dev/null
+++ b/packages/hooks/use-pagination/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@nextui-org/use-pagination",
+ "version": "1.0.0-beta.11",
+ "description": "State management hook for Pagination component, it lets you manage pagination with controlled and uncontrolled state",
+ "keywords": [
+ "use-pagination"
+ ],
+ "author": "Junior Garcia ",
+ "homepage": "https://nextui.org",
+ "license": "MIT",
+ "main": "src/index.ts",
+ "sideEffects": false,
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/nextui-org/nextui.git",
+ "directory": "packages/hooks/use-pagination"
+ },
+ "bugs": {
+ "url": "https://github.com/nextui-org/nextui/issues"
+ },
+ "scripts": {
+ "build": "tsup src/index.ts --format=esm,cjs --dts",
+ "dev": "yarn build:fast -- --watch",
+ "clean": "rimraf dist .turbo",
+ "typecheck": "tsc --noEmit",
+ "build:fast": "tsup src/index.ts --format=esm,cjs",
+ "prepack": "clean-package",
+ "postpack": "clean-package restore"
+ },
+ "dependencies": {
+ "@nextui-org/shared-utils": "workspace:*"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "devDependencies": {
+ "clean-package": "2.1.1",
+ "react": "^17.0.2"
+ }
+}
diff --git a/packages/hooks/use-pagination/src/index.ts b/packages/hooks/use-pagination/src/index.ts
new file mode 100644
index 000000000..db388b2e5
--- /dev/null
+++ b/packages/hooks/use-pagination/src/index.ts
@@ -0,0 +1,119 @@
+import {useMemo, useCallback, useState, useEffect} from "react";
+import {range} from "@nextui-org/shared-utils";
+
+export interface UsePaginationProps {
+ /**
+ * The total number of pages.
+ */
+ total: number;
+ /**
+ * The selected page on initial render.
+ * @default 1
+ */
+ initialPage?: number;
+ /**
+ * The controlled selected page.
+ */
+ page?: number;
+ /**
+ * The number of pages to show on each side of the current page.
+ * @default 1
+ */
+ siblings?: number;
+ /**
+ * The number of pages to show at the beginning and end of the pagination.
+ * @default 1
+ */
+ boundaries?: number;
+ /**
+ * Callback fired when the page changes.
+ */
+ onChange?: (page: number) => void;
+}
+
+export const DOTS = "dots";
+export type PaginationItemParam = number | typeof DOTS;
+
+export function usePagination(props: UsePaginationProps) {
+ const {page, total, siblings = 1, boundaries = 1, initialPage = 1, onChange} = props;
+ const [activePage, setActivePage] = useState(page || initialPage);
+
+ const onChangeActivePage = (newPage: number) => {
+ setActivePage(newPage);
+ onChange && onChange(newPage);
+ };
+
+ useEffect(() => {
+ if (page && page !== activePage) {
+ setActivePage(page);
+ }
+ }, [page]);
+
+ const setPage = useCallback(
+ (pageNumber: number) => {
+ if (pageNumber <= 0) {
+ onChangeActivePage(1);
+ } else if (pageNumber > total) {
+ onChangeActivePage(total);
+ } else {
+ onChangeActivePage(pageNumber);
+ }
+ },
+ [total, activePage],
+ );
+
+ const next = () => setPage(activePage + 1);
+ const previous = () => setPage(activePage - 1);
+ const first = () => setPage(1);
+ const last = () => setPage(total);
+
+ const paginationRange = useMemo((): PaginationItemParam[] => {
+ const totalPageNumbers = siblings * 2 + 3 + boundaries * 2;
+
+ if (totalPageNumbers >= total) {
+ return range(1, total);
+ }
+ const leftSiblingIndex = Math.max(activePage - siblings, boundaries);
+ const rightSiblingIndex = Math.min(activePage + siblings, total - boundaries);
+
+ /*
+ * We do not want to show dots if there is only one position left
+ * after/before the left/right page count as that would lead to a change if our Pagination
+ * component size which we do not want
+ */
+ const shouldShowLeftDots = leftSiblingIndex > boundaries + 2;
+ const shouldShowRightDots = rightSiblingIndex < total - (boundaries + 1);
+
+ if (!shouldShowLeftDots && shouldShowRightDots) {
+ const leftItemCount = siblings * 2 + boundaries + 2;
+
+ return [...range(1, leftItemCount), DOTS, ...range(total - (boundaries - 1), total)];
+ }
+
+ if (shouldShowLeftDots && !shouldShowRightDots) {
+ const rightItemCount = boundaries + 1 + 2 * siblings;
+
+ return [...range(1, boundaries), DOTS, ...range(total - rightItemCount, total)];
+ }
+
+ return [
+ ...range(1, boundaries),
+ DOTS,
+ ...range(leftSiblingIndex, rightSiblingIndex),
+ DOTS,
+ ...range(total - boundaries + 1, total),
+ ];
+ }, [total, siblings, activePage]);
+
+ return {
+ range: paginationRange,
+ active: activePage,
+ setPage,
+ next,
+ previous,
+ first,
+ last,
+ };
+}
+
+export type UsePaginationReturn = ReturnType;
diff --git a/packages/hooks/use-pagination/tsconfig.json b/packages/hooks/use-pagination/tsconfig.json
new file mode 100644
index 000000000..46e3b466c
--- /dev/null
+++ b/packages/hooks/use-pagination/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../../tsconfig.json",
+ "include": ["src", "index.ts"]
+}
diff --git a/packages/hooks/use-pagination/tsup.config.ts b/packages/hooks/use-pagination/tsup.config.ts
new file mode 100644
index 000000000..8bcd44aa9
--- /dev/null
+++ b/packages/hooks/use-pagination/tsup.config.ts
@@ -0,0 +1,13 @@
+import {defineConfig} from "tsup";
+import {findUpSync} from "find-up";
+
+export default defineConfig({
+ clean: true,
+ minify: false,
+ treeshake: true,
+ format: ["cjs", "esm"],
+ outExtension(ctx) {
+ return {js: `.${ctx.format}.js`};
+ },
+ inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined,
+});
diff --git a/packages/utilities/shared-utils/src/index.ts b/packages/utilities/shared-utils/src/index.ts
index 0f221e6cc..441e9ec01 100644
--- a/packages/utilities/shared-utils/src/index.ts
+++ b/packages/utilities/shared-utils/src/index.ts
@@ -11,3 +11,4 @@ export * from "./dimensions";
export * from "./css-transition";
export * from "./functions";
export * from "./context";
+export * from "./numbers";
diff --git a/packages/utilities/shared-utils/src/numbers.ts b/packages/utilities/shared-utils/src/numbers.ts
new file mode 100644
index 000000000..a3c680ba1
--- /dev/null
+++ b/packages/utilities/shared-utils/src/numbers.ts
@@ -0,0 +1,11 @@
+/**
+ * Returns an array of numbers, starting at `start` and ending at `end`.
+ * @param start number
+ * @param end number
+ * @returns number[]
+ */
+export function range(start: number, end: number) {
+ const length = end - start + 1;
+
+ return Array.from({length}, (_, index) => index + start);
+}
diff --git a/plop/hook/src/index.ts.hbs b/plop/hook/src/index.ts.hbs
index 9d352759d..73b8d424e 100644
--- a/plop/hook/src/index.ts.hbs
+++ b/plop/hook/src/index.ts.hbs
@@ -1,7 +1,7 @@
-export interface Use{{capitalize hookName}}Props {}
+export interface {{capitalize hookName}}Props {}
-export function {{camelCase hookName}}(props: Use{{capitalize hookName}}Props = {}) {
+export function {{camelCase hookName}}(props: {{capitalize hookName}}Props = {}) {
const {...otherProps} = props;
return {...otherProps};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index af8255b2b..1907f6930 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -637,6 +637,29 @@ importers:
clean-package: 2.1.1
react: 17.0.2
+ packages/components/pagination:
+ specifiers:
+ '@nextui-org/dom-utils': workspace:*
+ '@nextui-org/grid': workspace:*
+ '@nextui-org/shared-css': workspace:*
+ '@nextui-org/shared-utils': workspace:*
+ '@nextui-org/system': workspace:*
+ '@nextui-org/use-pagination': workspace:*
+ '@react-aria/utils': ^3.14.0
+ clean-package: 2.1.1
+ react: ^17.0.2
+ dependencies:
+ '@nextui-org/dom-utils': link:../../utilities/dom-utils
+ '@nextui-org/grid': link:../grid
+ '@nextui-org/shared-css': link:../../utilities/shared-css
+ '@nextui-org/shared-utils': link:../../utilities/shared-utils
+ '@nextui-org/system': link:../../core/system
+ '@nextui-org/use-pagination': link:../../hooks/use-pagination
+ '@react-aria/utils': 3.14.0_react@17.0.2
+ devDependencies:
+ clean-package: 2.1.1
+ react: 17.0.2
+
packages/components/row:
specifiers:
'@nextui-org/dom-utils': workspace:*
@@ -742,6 +765,7 @@ importers:
'@nextui-org/image': workspace:*
'@nextui-org/link': workspace:*
'@nextui-org/loading': workspace:*
+ '@nextui-org/pagination': workspace:*
'@nextui-org/row': workspace:*
'@nextui-org/snippet': workspace:*
'@nextui-org/spacer': workspace:*
@@ -766,6 +790,7 @@ importers:
'@nextui-org/image': link:../../components/image
'@nextui-org/link': link:../../components/link
'@nextui-org/loading': link:../../components/loading
+ '@nextui-org/pagination': link:../../components/pagination
'@nextui-org/row': link:../../components/row
'@nextui-org/snippet': link:../../components/snippet
'@nextui-org/spacer': link:../../components/spacer
@@ -804,6 +829,17 @@ importers:
clean-package: 2.1.1
react: 17.0.2
+ packages/hooks/use-pagination:
+ specifiers:
+ '@nextui-org/shared-utils': workspace:*
+ clean-package: 2.1.1
+ react: ^17.0.2
+ dependencies:
+ '@nextui-org/shared-utils': link:../../utilities/shared-utils
+ devDependencies:
+ clean-package: 2.1.1
+ react: 17.0.2
+
packages/hooks/use-real-shape:
specifiers:
'@nextui-org/dom-utils': workspace:*