diff --git a/src/components/TablePagination/TablePagination.scss b/src/components/TablePagination/TablePagination.scss
new file mode 100644
index 000000000..a7756b72c
--- /dev/null
+++ b/src/components/TablePagination/TablePagination.scss
@@ -0,0 +1,40 @@
+@import "~vanilla-framework/scss/settings";
+
+.pagination {
+ align-items: baseline;
+ display: flex;
+ margin-top: 1.2rem;
+
+ .description {
+ flex-grow: 1;
+ }
+
+ .back {
+ margin: 0 $spv--large;
+
+ .p-icon--chevron-down {
+ rotate: 90deg;
+ }
+ }
+
+ .next {
+ margin: 0 $spv--large;
+
+ .p-icon--chevron-down {
+ rotate: 270deg;
+ }
+ }
+
+ .pagination-input {
+ margin-right: $spv--small;
+ min-width: 0;
+ width: 3rem;
+ }
+
+ .pagination-select {
+ margin-bottom: 0;
+ margin-left: $spv--x-large;
+ min-width: 0;
+ width: 7rem;
+ }
+}
diff --git a/src/components/TablePagination/TablePagination.stories.mdx b/src/components/TablePagination/TablePagination.stories.mdx
new file mode 100644
index 000000000..6679f2546
--- /dev/null
+++ b/src/components/TablePagination/TablePagination.stories.mdx
@@ -0,0 +1,214 @@
+import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
+
+import TablePagination from "./TablePagination";
+import MainTable from "../MainTable";
+
+
+
+export const Template = (args) => ;
+
+### TablePagination
+
+This is an HOC [React](https://reactjs.org/) component for applying pagination to input data for direct child components.
+This component is un-opinionated about the structure of the input data and can be used with any child component that displays
+a list of data. However, the styling and behaviour of this component were designed to work nicely with the ```MainTable``` component.
+
+To use this component, simply wrap a child component with it and provide the data that you want to paginate to the ```data``` prop.
+This component will then pass the paged data to all direct child components via a child prop specified by ```dataForwardProp```.
+
+### Props
+
+
+
+### Default
+
+
+
+### Custom page limit
+
+
+
+### Custom display title
+
+
+
+### Render above
+
+
+
+### Render below
+
+
diff --git a/src/components/TablePagination/TablePagination.test.tsx b/src/components/TablePagination/TablePagination.test.tsx
new file mode 100644
index 000000000..6093dbee1
--- /dev/null
+++ b/src/components/TablePagination/TablePagination.test.tsx
@@ -0,0 +1,90 @@
+/* eslint-disable testing-library/no-node-access */
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import TablePagination from "./TablePagination";
+import userEvent from "@testing-library/user-event";
+
+const dummyData = [
+ { id: "row-1" },
+ { id: "row-2" },
+ { id: "row-3" },
+ { id: "row-4" },
+ { id: "row-5" },
+];
+
+describe("", () => {
+ // snapshot tests
+ it("renders table pagination and matches the snapshot", () => {
+ render();
+
+ expect(screen.getByRole("navigation")).toMatchSnapshot();
+ });
+
+ // unit tests
+ it("renders default display title correctly when no pagination takes place", () => {
+ render();
+
+ expect(screen.getByRole("navigation")).toHaveTextContent(
+ "Showing all 5 items"
+ );
+ });
+
+ it("renders default display title correctly when pagination takes place", () => {
+ render();
+
+ expect(screen.getByRole("navigation")).toHaveTextContent(
+ "Showing 1 out of 5 items"
+ );
+ });
+
+ it("has correct per page setting when changed", () => {
+ render();
+
+ expect(screen.getByRole("navigation")).toHaveTextContent("2/page");
+ userEvent.selectOptions(
+ screen.getByRole("combobox", { name: "Items per page" }),
+ "5"
+ );
+ expect(screen.getByRole("navigation")).toHaveTextContent("5/page");
+ });
+
+ it("resets to first page when page size is changed", () => {
+ render();
+
+ expect(screen.getByRole("navigation")).toHaveTextContent("2/page");
+ userEvent.selectOptions(
+ screen.getByRole("combobox", { name: "Items per page" }),
+ "5"
+ );
+ const currentPageInput = screen.getByRole("spinbutton", {
+ name: "Page number",
+ });
+ expect(currentPageInput).toHaveValue(1);
+ });
+
+ it("should paginate correctly in incrementing or decrementing directions", async () => {
+ render();
+ const incButton = screen.getByRole("button", { name: "Next page" });
+ const decButton = screen.getByRole("button", { name: "Previous page" });
+ const currentPageInput = screen.getByRole("spinbutton", {
+ name: "Page number",
+ });
+ const pageSizeSelector = screen.getByRole("combobox", {
+ name: "Items per page",
+ });
+
+ expect(currentPageInput).toHaveValue(1);
+ await userEvent.click(decButton);
+ expect(currentPageInput).toHaveValue(1);
+ await userEvent.click(incButton);
+ expect(currentPageInput).toHaveValue(2);
+ await userEvent.selectOptions(pageSizeSelector, "2");
+ expect(currentPageInput).toHaveValue(1);
+ await fireEvent.change(currentPageInput, { target: { value: 3 } });
+ expect(currentPageInput).toHaveValue(3);
+ await userEvent.click(incButton);
+ expect(currentPageInput).toHaveValue(3);
+ await userEvent.click(decButton);
+ expect(currentPageInput).toHaveValue(2);
+ });
+});
diff --git a/src/components/TablePagination/TablePagination.tsx b/src/components/TablePagination/TablePagination.tsx
new file mode 100644
index 000000000..c6ec1f35d
--- /dev/null
+++ b/src/components/TablePagination/TablePagination.tsx
@@ -0,0 +1,180 @@
+import React, {
+ ChangeEvent,
+ Children,
+ HTMLAttributes,
+ PropsWithChildren,
+ ReactElement,
+ ReactNode,
+ RefObject,
+ cloneElement,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import classnames from "classnames";
+import { usePagination } from "hooks";
+import Select from "components/Select";
+import TablePaginationControls from "./TablePaginationControls";
+import "./TablePagination.scss";
+
+/**
+ * Determine if we are working with a small screen.
+ * 'small screen' in this case is relative to the width of the description div
+ */
+const figureSmallScreen = (descriptionRef: RefObject) => {
+ const descriptionElement = descriptionRef.current;
+ if (!descriptionElement) {
+ return true;
+ }
+ return descriptionElement.getBoundingClientRect().width < 230;
+};
+
+/**
+ * Iterate direct react child components and override the value of the prop specified by @param dataForwardProp
+ * for those child components.
+ * @param children - react node children to iterate
+ * @param dataForwardProp - the name of the prop from the children components to override
+ * @param data - actual data to be passed to the prop specified by @param dataForwardProp
+ */
+const renderChildren = (
+ children: ReactNode,
+ dataForwardProp: string,
+ data: unknown[]
+) => {
+ return Children.map(children, (child) => {
+ return cloneElement(child as ReactElement, {
+ [dataForwardProp]: data,
+ });
+ });
+};
+
+const DEFAULT_PAGE_LIMITS = [50, 100, 200];
+const generatePagingOptions = (pageLimits: number[]) => {
+ return pageLimits.map((limit) => ({ value: limit, label: `${limit}/page` }));
+};
+
+export type Props = PropsWithChildren<{
+ /**
+ * list of data elements to be paginated. This component is un-opinionated about
+ * the structure of the data but it should be identical to the data structure
+ * reuiqred by the child table component
+ */
+ data: unknown[];
+ /**
+ * prop name of the child table component that receives paginated data.
+ * default value is set to @constant rows, which is the data prop for the @func MainTable component
+ */
+ dataForwardProp?: string;
+ /**
+ * the name of the item associated to each row within the table.
+ */
+ itemName?: string;
+ /**
+ * custom styling for the pagination container
+ */
+ className?: string;
+ /**
+ * custom description to be displayed by the pagination
+ */
+ description?: ReactNode;
+ /**
+ * custom per page limits express as an array of numbers.
+ */
+ pageLimits?: number[];
+ /**
+ * place the pagination component above or below the table?
+ */
+ position?: "above" | "below";
+}> &
+ HTMLAttributes;
+
+const TablePagination = ({
+ data,
+ className,
+ itemName = "item",
+ description,
+ position = "above",
+ dataForwardProp = "rows",
+ pageLimits = DEFAULT_PAGE_LIMITS,
+ children,
+ ...divProps
+}: Props) => {
+ const descriptionRef = useRef(null);
+ const [isSmallScreen, setSmallScreen] = useState(false);
+ const [pageSize, setPageSize] = useState(() => {
+ return generatePagingOptions(pageLimits)[0].value;
+ });
+ const { paginate, currentPage, pageData, totalItems } = usePagination(data, {
+ itemsPerPage: pageSize,
+ autoResetPage: true,
+ });
+
+ useEffect(() => {
+ const handleResize = () => {
+ setSmallScreen(figureSmallScreen(descriptionRef));
+ };
+ window.addEventListener("resize", handleResize);
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ };
+ }, [isSmallScreen]);
+
+ const handlePageSizeChange = (e: ChangeEvent) => {
+ paginate(1);
+ setPageSize(parseInt(e.target.value));
+ };
+
+ const getDescription = () => {
+ if (description) {
+ return description;
+ }
+
+ const visibleCount = pageData.length;
+
+ if (isSmallScreen) {
+ return `${visibleCount} out of ${totalItems}`;
+ }
+
+ if (visibleCount === totalItems && visibleCount > 1) {
+ return `Showing all ${totalItems} ${itemName}s`;
+ }
+
+ return `Showing ${visibleCount} out of ${totalItems} ${itemName}${
+ totalItems !== 1 ? "s" : ""
+ }`;
+ };
+
+ const totalPages = Math.ceil(data.length / pageSize);
+ const clonedChildren = renderChildren(children, dataForwardProp, pageData);
+ return (
+ <>
+ {position === "below" && clonedChildren}
+
+
+ {getDescription()}
+
+
+
+
+ {position === "above" && clonedChildren}
+ >
+ );
+};
+
+export default TablePagination;
diff --git a/src/components/TablePagination/TablePaginationControls/TablePaginationControls.test.tsx b/src/components/TablePagination/TablePaginationControls/TablePaginationControls.test.tsx
new file mode 100644
index 000000000..5e7edbcfd
--- /dev/null
+++ b/src/components/TablePagination/TablePaginationControls/TablePaginationControls.test.tsx
@@ -0,0 +1,21 @@
+/* eslint-disable testing-library/no-node-access */
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import TablePaginationControls from "./TablePaginationControls";
+
+describe("", () => {
+ // snapshot tests
+ it("renders table pagination controls and matches the snapshot", () => {
+ render(
+
+ );
+
+ expect(screen.getAllByRole("button")).toMatchSnapshot();
+ expect(screen.getByRole("spinbutton")).toMatchSnapshot();
+ });
+});
diff --git a/src/components/TablePagination/TablePaginationControls/TablePaginationControls.tsx b/src/components/TablePagination/TablePaginationControls/TablePaginationControls.tsx
new file mode 100644
index 000000000..b7ff267ff
--- /dev/null
+++ b/src/components/TablePagination/TablePaginationControls/TablePaginationControls.tsx
@@ -0,0 +1,79 @@
+import Button from "components/Button";
+import Icon from "components/Icon";
+import Input from "components/Input";
+import React, { ChangeEvent } from "react";
+
+export type Props = {
+ /**
+ * Callback function to handle a change in page number
+ */
+ onPageChange: (pageNumber: number) => void;
+ /**
+ * The current page of the data
+ */
+ currentPage: number;
+ /**
+ * The total number of pages that exists within the data
+ */
+ totalPages: number;
+};
+
+const TablePaginationControls = ({
+ onPageChange,
+ currentPage,
+ totalPages,
+}: Props): JSX.Element => {
+ const handleDecrementPage = (currentPage: number) => {
+ if (currentPage > 1) {
+ onPageChange(currentPage - 1);
+ }
+ };
+
+ const handleIncrementPage = (currentPage: number, totalPages: number) => {
+ if (currentPage < totalPages) {
+ onPageChange(currentPage + 1);
+ }
+ };
+
+ const handleInputPageChange = (e: ChangeEvent) => {
+ const newPage = Math.min(totalPages, Math.max(1, parseInt(e.target.value)));
+ onPageChange(newPage);
+ };
+
+ return (
+ <>
+
+ {" "}
+ of {totalPages}
+
+ >
+ );
+};
+
+export default TablePaginationControls;
diff --git a/src/components/TablePagination/TablePaginationControls/__snapshots__/TablePaginationControls.test.tsx.snap b/src/components/TablePagination/TablePaginationControls/__snapshots__/TablePaginationControls.test.tsx.snap
new file mode 100644
index 000000000..3fc41511a
--- /dev/null
+++ b/src/components/TablePagination/TablePaginationControls/__snapshots__/TablePaginationControls.test.tsx.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders table pagination controls and matches the snapshot 1`] = `
+Array [
+ ,
+ ,
+]
+`;
+
+exports[` renders table pagination controls and matches the snapshot 2`] = `
+
+`;
diff --git a/src/components/TablePagination/TablePaginationControls/index.ts b/src/components/TablePagination/TablePaginationControls/index.ts
new file mode 100644
index 000000000..48aa68113
--- /dev/null
+++ b/src/components/TablePagination/TablePaginationControls/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./TablePaginationControls";
+export type { Props as TablePaginationControlsProps } from "./TablePaginationControls";
diff --git a/src/components/TablePagination/__snapshots__/TablePagination.test.tsx.snap b/src/components/TablePagination/__snapshots__/TablePagination.test.tsx.snap
new file mode 100644
index 000000000..ee4d0a51a
--- /dev/null
+++ b/src/components/TablePagination/__snapshots__/TablePagination.test.tsx.snap
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` renders table pagination and matches the snapshot 1`] = `
+
+`;
diff --git a/src/components/TablePagination/index.ts b/src/components/TablePagination/index.ts
new file mode 100644
index 000000000..aac664179
--- /dev/null
+++ b/src/components/TablePagination/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./TablePagination";
+export type { Props as TablePaginationProps } from "./TablePagination";
diff --git a/src/index.ts b/src/index.ts
index 44ce5f406..ad41e8f08 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -64,6 +64,7 @@ export { default as TableRow } from "./components/TableRow";
export { default as Tabs } from "./components/Tabs";
export { default as Textarea } from "./components/Textarea";
export { default as Tooltip } from "./components/Tooltip";
+export { default as TablePagination } from "./components/TablePagination";
export type { AccordionProps } from "./components/Accordion";
export type { ActionButtonProps } from "./components/ActionButton";
@@ -134,6 +135,7 @@ export type { TableRowProps } from "./components/TableRow";
export type { TabsProps } from "./components/Tabs";
export type { TextareaProps } from "./components/Textarea";
export type { TooltipProps } from "./components/Tooltip";
+export type { TablePaginationProps } from "./components/TablePagination";
export {
useOnClickOutside,