diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/Skeleton.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/Skeleton.md
new file mode 100644
index 00000000..1286c024
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/Skeleton.md
@@ -0,0 +1,38 @@
+---
+# Sidenav top-level section
+# should be the same for all markdown files
+section: extensions
+subsection: Component groups
+# Sidenav secondary level section
+# should be the same for all markdown files
+id: Skeleton table
+# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
+source: react
+# If you use typescript, the name of the interface to display props for
+# These are found through the sourceProps function provided in patternfly-docs.source.js
+propComponents: ['SkeletonTable']
+---
+import SkeletonTable from '@patternfly/react-component-groups/dist/dynamic/SkeletonTable';
+
+The **skeleton table** component is used to display placeholder "skeletons" within a table as its contents load.
+
+## Examples
+
+### Basic skeleton table
+
+To indicate that a table's cells are still loading, a basic skeleton table uses the [skeleton](https://www.patternfly.org/components/skeleton) component to place a placeholder skeleton in each cell. Once the data is loaded, the skeleton table is replaced with a table containing the real data.
+
+```js file="./SkeletonTableExample.tsx"
+
+```
+
+### Full loading simulation
+
+The following example demonstrates the typical behavior of a skeleton table transitioning to a normal table as the data becomes available.
+
+To simulate this loading process, select the `Reload table` button and wait for the data to populate.
+
+
+```js file="./SkeletonTableLoadingExample.tsx"
+
+```
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/SkeletonTableExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/SkeletonTableExample.tsx
new file mode 100644
index 00000000..2da39452
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/SkeletonTableExample.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+import SkeletonTable from '@patternfly/react-core/dist/js/components/Skeleton/SkeletonTable';
+
+export const SkeletonTableExample: React.FC = () =>
\ No newline at end of file
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/SkeletonTableLoadingExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/SkeletonTableLoadingExample.tsx
new file mode 100644
index 00000000..c12d7b83
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Skeleton/SkeletonTableLoadingExample.tsx
@@ -0,0 +1,101 @@
+import React from 'react';
+import SkeletonTable from '@patternfly/react-core/dist/js/components/Skeleton/SkeletonTable';
+import { Table, Tbody, Td, Th, Tr, Thead } from '@patternfly/react-table';
+import { Button, Stack, StackItem } from '@patternfly/react-core';
+
+interface Repository {
+ name: string;
+ branches: string | null;
+ prs: string | null;
+ workspaces: string;
+ lastCommit: string;
+}
+
+export const SkeletonTableExample: React.FC = () => {
+ const [ isLoaded, setIsLoaded ] = React.useState(false);
+
+ const simulatedAsyncCall = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(true);
+ }, 5000);
+ });
+
+ const loadData = async () => {
+ const result = await simulatedAsyncCall;
+ setIsLoaded(result);
+ };
+
+ const repositories: Repository[] = [
+ { name: 'one', branches: 'two', prs: 'three', workspaces: 'four', lastCommit: 'five' },
+ { name: 'one - 2', branches: null, prs: null, workspaces: 'four - 2', lastCommit: 'five - 2' },
+ { name: 'one - 3', branches: 'two - 3', prs: 'three - 3', workspaces: 'four - 3', lastCommit: 'five - 3' }
+ ];
+
+ const columnNames = {
+ name: 'Repositories',
+ branches: 'Branches',
+ prs: 'Pull requests',
+ workspaces: 'Workspaces',
+ lastCommit: 'Last commit'
+ };
+
+ let table: React.ReactNode;
+
+ if (!isLoaded) {
+ table = (
+
+ );
+ } else {
+ table = (
+
+
+
+ {columnNames.name} |
+ {columnNames.branches} |
+ {columnNames.prs} |
+ {columnNames.workspaces} |
+ {columnNames.lastCommit} |
+
+
+
+ {repositories.map((repo) => (
+
+ {repo.name} |
+ {repo.branches} |
+ {repo.prs} |
+ {repo.workspaces} |
+ {repo.lastCommit} |
+
+ ))}
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {table}
+
+
+
+
+ >
+ );
+};
diff --git a/packages/module/src/SkeletonTable/SkeletonTable.test.tsx b/packages/module/src/SkeletonTable/SkeletonTable.test.tsx
new file mode 100644
index 00000000..72a426d5
--- /dev/null
+++ b/packages/module/src/SkeletonTable/SkeletonTable.test.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import SkeletonTable from './SkeletonTable';
+
+describe('SkeletonTable component', () => {
+ it('should render correctly', () => {
+ expect(render()).toMatchSnapshot();
+ });
+
+ it('should render correctly with rows', () => {
+ expect(render()).toMatchSnapshot();
+ });
+});
diff --git a/packages/module/src/SkeletonTable/SkeletonTable.tsx b/packages/module/src/SkeletonTable/SkeletonTable.tsx
new file mode 100644
index 00000000..f45a4971
--- /dev/null
+++ b/packages/module/src/SkeletonTable/SkeletonTable.tsx
@@ -0,0 +1,61 @@
+import React, { ReactNode } from 'react';
+import { Caption, Table, TableProps, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
+import { Skeleton } from '@patternfly/react-core';
+
+export type SkeletonTableProps = TableProps & {
+ /** Indicates the table variant */
+ variant?: TableVariant;
+ /** The number of rows the skeleton table should contain */
+ rows?: number;
+ /** Any captions that should be added to the table */
+ caption?: ReactNode;
+} & (
+ | {
+ columns: ReactNode[];
+ }
+ | {
+ numberOfColumns: number;
+ }
+ );
+
+
+function hasCustomColumns(props: Record): props is SkeletonTableProps & {
+ columns: ReactNode[];
+} {
+ return Array.isArray(props.columns);
+}
+
+const SkeletonTable: React.FunctionComponent = (props: SkeletonTableProps) => {
+ const { variant, rows = 5, caption } = props;
+ const rowCells = hasCustomColumns(props) ? props.columns.length : props.numberOfColumns;
+ const rowArray = [ ...new Array(rowCells) ];
+ const bodyRows = [ ...new Array(rows) ].map((_, index) => (
+
+ {rowArray.map((_, index) => (
+
+
+ |
+ ))}
+
+ ));
+
+ return (
+
+ {caption && {caption}}
+
+
+ {hasCustomColumns(props)
+ ? props.columns.map((c, index) => {c} | )
+ : rowArray.map((_, index) => (
+
+
+ |
+ ))}
+
+
+ {bodyRows}
+
+ );
+};
+
+export default SkeletonTable;
diff --git a/packages/module/src/SkeletonTable/__snapshots__/SkeletonTable.test.tsx.snap b/packages/module/src/SkeletonTable/__snapshots__/SkeletonTable.test.tsx.snap
new file mode 100644
index 00000000..dc85e3f0
--- /dev/null
+++ b/packages/module/src/SkeletonTable/__snapshots__/SkeletonTable.test.tsx.snap
@@ -0,0 +1,903 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SkeletonTable component should render correctly 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+
+
+ first
+ |
+
+ second
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ ,
+ "container":
+
+
+
+
+ first
+ |
+
+ second
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
+
+exports[`SkeletonTable component should render correctly with rows 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+
+
+ first
+ |
+
+ second
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ ,
+ "container":
+
+
+
+
+ first
+ |
+
+ second
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
diff --git a/packages/module/src/SkeletonTable/index.ts b/packages/module/src/SkeletonTable/index.ts
new file mode 100644
index 00000000..7e0a19d0
--- /dev/null
+++ b/packages/module/src/SkeletonTable/index.ts
@@ -0,0 +1,2 @@
+export { default } from './SkeletonTable';
+export { default as SkeletonTable } from './SkeletonTable';
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts
index 437bd712..84a1bf1d 100644
--- a/packages/module/src/index.ts
+++ b/packages/module/src/index.ts
@@ -27,6 +27,9 @@ export * from './HorizontalNav';
export { default as NotAuthorized } from './NotAuthorized';
export * from './NotAuthorized';
+export { default as SkeletonTable } from './SkeletonTable';
+export * from './SkeletonTable';
+
export { default as TagCount } from './TagCount';
export * from './TagCount';