diff --git a/example/src/App.js b/example/src/App.js
index d22c75d7..f5d08254 100644
--- a/example/src/App.js
+++ b/example/src/App.js
@@ -28,6 +28,7 @@ import {
Typography,
Modal,
Kite,
+ Loader,
// IMPORT_INJECTOR
} from '@cision/rover-ui';
@@ -474,6 +475,14 @@ const App = () => {
Success Kite!
+
{/** USAGE_INJECTOR */}
diff --git a/src/components/Loader/Loader.module.css b/src/components/Loader/Loader.module.css
new file mode 100644
index 00000000..a423a407
--- /dev/null
+++ b/src/components/Loader/Loader.module.css
@@ -0,0 +1,80 @@
+@keyframes LOADERRIPPLE {
+ 0% {
+ transform: scale(0, 0);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: scale(0.9, 0.9);
+ opacity: 0;
+ }
+}
+
+.Loader {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ justify-content: center;
+}
+
+.Loader::before {
+ content: '';
+ background-color: transparent;
+ position: absolute;
+ opacity: 1;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ border: 5px solid #0092c2;
+ animation: LOADERRIPPLE 1.8s infinite;
+ transform: scale(0, 0);
+ animation-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1);
+ animation-delay: 0s;
+}
+
+.Loader::after {
+ content: '';
+ background-color: transparent;
+ position: absolute;
+ opacity: 1;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ border: 5px solid #0092c2;
+ animation: LOADERRIPPLE 1.8s infinite;
+ transform: scale(0, 0);
+ animation-timing-function: cubic-bezier(0.3, 0.61, 0.335, 1);
+ animation-delay: -0.9s;
+}
+
+.sm,
+.sm::before,
+.sm::after {
+ width: 10px;
+ height: 10px;
+ margin: auto;
+}
+
+.md,
+.md::before,
+.md::after {
+ width: 20px;
+ height: 20px;
+ margin: auto;
+}
+
+.lg,
+.lg::before,
+.lg::after {
+ width: 50px;
+ height: 50px;
+ margin: auto;
+}
+
+.xl,
+.xl::before,
+.xl::after {
+ width: 100px;
+ height: 100px;
+ margin: auto;
+}
diff --git a/src/components/Loader/Loader.test.tsx b/src/components/Loader/Loader.test.tsx
new file mode 100644
index 00000000..26d90087
--- /dev/null
+++ b/src/components/Loader/Loader.test.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import { render } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import Loader from '.';
+
+describe('Loader', () => {
+ it('renders', () => {
+ render();
+ });
+
+ describe('when rendered with custom style with width and height', () => {
+ it('should override the default loader sizes', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const loaderElem = getByTestId('loaderElem') as HTMLDivElement;
+ expect(loaderElem.style.width).toBe('123px');
+ });
+ });
+
+ describe('when rendered with additional class names', () => {
+ it('should not replace the default classes', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const loaderElem = getByTestId('loaderElem') as HTMLDivElement;
+ const loaderClasses = loaderElem.className
+ .split(' ')
+ .filter((classes) => classes.match('\\w*(Loader)\\w*'));
+ expect(loaderClasses.length).toBe(2);
+ });
+ });
+});
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 00000000..3831f50a
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+import classNames from 'classnames';
+
+import styles from './Loader.module.css';
+
+export type Size = 'sm' | 'md' | 'lg' | 'xl';
+
+interface LoaderProps extends React.HTMLAttributes {
+ size?: Size;
+}
+
+const Loader: React.FC = ({ size = 'lg', ...rest }) => {
+ const mainClass = classNames(
+ styles.Loader,
+ rest.className,
+ `${styles[size]}`
+ );
+
+ const prop = {
+ ...rest,
+ className: mainClass,
+ };
+
+ return ;
+};
+
+export default Loader;
diff --git a/src/components/Loader/README.md b/src/components/Loader/README.md
new file mode 100644
index 00000000..79da27ec
--- /dev/null
+++ b/src/components/Loader/README.md
@@ -0,0 +1,16 @@
+# \
+
+render a loading icon
+
+### Valid Sizes
+
+Size is optional, defaults to large when omitted
+
+- sm
+- md
+- lg
+- xl
+
+```jsx
+
+```
diff --git a/src/components/Loader/index.ts b/src/components/Loader/index.ts
new file mode 100644
index 00000000..348c02a9
--- /dev/null
+++ b/src/components/Loader/index.ts
@@ -0,0 +1 @@
+export { default } from './Loader';
diff --git a/src/components/Loader/story.tsx b/src/components/Loader/story.tsx
new file mode 100644
index 00000000..4742eeab
--- /dev/null
+++ b/src/components/Loader/story.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { select } from '@storybook/addon-knobs';
+
+import Loader from './Loader';
+import Readme from './README.md';
+
+import { Wrap } from '../../stories/storybook-helpers';
+
+const FlexWrapper = (props) => (
+
+);
+
+storiesOf('Planets/Loader', module)
+ .addParameters({
+ readme: {
+ sidebar: Readme,
+ },
+ })
+ .add(
+ 'Overview',
+ () => (
+
+
+
+ ),
+ {
+ info: {
+ inline: false,
+ source: true,
+ },
+ }
+ )
+ .add(
+ 'Examples',
+ () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
+ {
+ info: {
+ inline: false,
+ source: true,
+ },
+ }
+ );
diff --git a/src/index.ts b/src/index.ts
index 1dfeb6ea..4d71b292 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -36,3 +36,4 @@ export { default as InputTime } from './components/InputTime';
export { default as Typography } from './components/Typography';
export { default as Modal } from './components/Modal';
export { default as Kite } from './components/Kite';
+export { default as Loader } from './components/Loader';
diff --git a/src/stories/index.js b/src/stories/index.js
index 6124ceb7..21259e0d 100644
--- a/src/stories/index.js
+++ b/src/stories/index.js
@@ -24,6 +24,7 @@ import '../components/Input/story';
import '../components/Input/Checkbox/story';
import '../components/InputTime/story';
import '../components/Typography/story';
+import '../components/Loader/story';
/*
* STAR SYSTEMS