diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11e157148e..5037fff422 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
+### Features
+- Add `Loader` component @layershifter ([#685](https://github.com/stardust-ui/react/pull/685))
+
## [v0.16.1](https://github.com/stardust-ui/react/tree/v0.16.1) (2019-01-10)
[Compare changes](https://github.com/stardust-ui/react/compare/v0.16.0...v0.16.1)
diff --git a/docs/src/examples/components/Loader/Types/LoaderExample.tsx b/docs/src/examples/components/Loader/Types/LoaderExample.tsx
new file mode 100644
index 0000000000..ba7aaf121d
--- /dev/null
+++ b/docs/src/examples/components/Loader/Types/LoaderExample.tsx
@@ -0,0 +1,6 @@
+import { Loader } from '@stardust-ui/react'
+import * as React from 'react'
+
+const LoaderExample: React.FC = () =>
+
+export default LoaderExample
diff --git a/docs/src/examples/components/Loader/Types/LoaderExampleLabel.shorthand.tsx b/docs/src/examples/components/Loader/Types/LoaderExampleLabel.shorthand.tsx
new file mode 100644
index 0000000000..43ca493472
--- /dev/null
+++ b/docs/src/examples/components/Loader/Types/LoaderExampleLabel.shorthand.tsx
@@ -0,0 +1,6 @@
+import { Loader } from '@stardust-ui/react'
+import * as React from 'react'
+
+const LoaderExampleLabel: React.FC = () =>
+
+export default LoaderExampleLabel
diff --git a/docs/src/examples/components/Loader/Types/index.tsx b/docs/src/examples/components/Loader/Types/index.tsx
new file mode 100644
index 0000000000..30769a5a64
--- /dev/null
+++ b/docs/src/examples/components/Loader/Types/index.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react'
+
+import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'
+
+const LoaderTypesExamples = () => (
+
+
+
+
+)
+
+export default LoaderTypesExamples
diff --git a/docs/src/examples/components/Loader/Variations/LoaderExampleInline.tsx b/docs/src/examples/components/Loader/Variations/LoaderExampleInline.tsx
new file mode 100644
index 0000000000..c11b08c161
--- /dev/null
+++ b/docs/src/examples/components/Loader/Variations/LoaderExampleInline.tsx
@@ -0,0 +1,6 @@
+import { Loader } from '@stardust-ui/react'
+import * as React from 'react'
+
+const LoaderExampleInline: React.FC = () =>
+
+export default LoaderExampleInline
diff --git a/docs/src/examples/components/Loader/Variations/LoaderExampleLabelPosition.shorthand.tsx b/docs/src/examples/components/Loader/Variations/LoaderExampleLabelPosition.shorthand.tsx
new file mode 100644
index 0000000000..677335a880
--- /dev/null
+++ b/docs/src/examples/components/Loader/Variations/LoaderExampleLabelPosition.shorthand.tsx
@@ -0,0 +1,14 @@
+import { Grid, Loader } from '@stardust-ui/react'
+import * as React from 'react'
+
+const LoaderExampleLabel: React.FC = () => (
+
+
+
+
+
+
+
+)
+
+export default LoaderExampleLabel
diff --git a/docs/src/examples/components/Loader/Variations/LoaderExampleSize.tsx b/docs/src/examples/components/Loader/Variations/LoaderExampleSize.tsx
new file mode 100644
index 0000000000..fbd19090f9
--- /dev/null
+++ b/docs/src/examples/components/Loader/Variations/LoaderExampleSize.tsx
@@ -0,0 +1,16 @@
+import { Grid, Loader } from '@stardust-ui/react'
+import * as React from 'react'
+
+const LoaderExampleSize: React.FC = () => (
+
+
+
+
+
+
+
+
+
+)
+
+export default LoaderExampleSize
diff --git a/docs/src/examples/components/Loader/Variations/index.tsx b/docs/src/examples/components/Loader/Variations/index.tsx
new file mode 100644
index 0000000000..de97527f08
--- /dev/null
+++ b/docs/src/examples/components/Loader/Variations/index.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react'
+
+import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'
+
+const LoaderTypesExamples = () => (
+
+
+
+
+
+)
+
+export default LoaderTypesExamples
diff --git a/docs/src/examples/components/Loader/index.tsx b/docs/src/examples/components/Loader/index.tsx
new file mode 100644
index 0000000000..e43a41ece4
--- /dev/null
+++ b/docs/src/examples/components/Loader/index.tsx
@@ -0,0 +1,13 @@
+import * as React from 'react'
+
+import Types from './Types'
+import Variations from './Variations'
+
+const LoaderExamples = () => (
+ <>
+
+
+ >
+)
+
+export default LoaderExamples
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000000..f39504385f
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,89 @@
+import * as PropTypes from 'prop-types'
+import * as React from 'react'
+
+import {
+ UIComponent,
+ createShorthandFactory,
+ UIComponentProps,
+ commonPropTypes,
+ ColorComponentProps,
+ customPropTypes,
+} from '../../lib'
+import { loaderBehavior } from '../../lib/accessibility'
+import { Accessibility } from '../../lib/accessibility/types'
+import { ReactProps, ShorthandValue } from '../../../types/utils'
+import Slot from '../Slot/Slot'
+
+export type LoaderPosition = 'above' | 'below' | 'start' | 'end'
+export type LoaderSize =
+ | 'smallest'
+ | 'smaller'
+ | 'small'
+ | 'medium'
+ | 'large'
+ | 'larger'
+ | 'largest'
+
+export interface LoaderProps extends UIComponentProps, ColorComponentProps {
+ /**
+ * Accessibility behavior if overridden by the user.
+ * @default defaultBehavior
+ */
+ accessibility?: Accessibility
+
+ /** A loader can contain an indicator. */
+ indicator?: ShorthandValue
+
+ /** A loader can contain a label. */
+ label?: ShorthandValue
+
+ /** A label in the loader can have different positions. */
+ labelPosition?: LoaderPosition
+
+ /** A size of the loader. */
+ size?: LoaderSize
+}
+
+/**
+ * A Loader indicates a possible user action.
+ */
+class Loader extends UIComponent> {
+ static create: Function
+ static displayName = 'Loader'
+ static className = 'ui-loader'
+
+ static propTypes = {
+ ...commonPropTypes.createCommon({
+ children: false,
+ content: false,
+ color: true,
+ }),
+ accessibility: PropTypes.func,
+ indicator: customPropTypes.itemShorthand,
+ label: customPropTypes.itemShorthand,
+ labelPosition: PropTypes.oneOf(['above', 'below', 'start', 'end']),
+ size: PropTypes.oneOf(['smallest', 'smaller', 'small', 'medium', 'large', 'larger', 'largest']),
+ }
+
+ static defaultProps = {
+ accessibility: loaderBehavior,
+ indicator: '',
+ labelPosition: 'below',
+ size: 'medium',
+ }
+
+ renderComponent({ ElementType, classes, accessibility, variables, styles, unhandledProps }) {
+ const { indicator, label } = this.props
+
+ return (
+
+ {Slot.create(indicator, { defaultProps: { styles: styles.indicator } })}
+ {Slot.create(label, { defaultProps: { styles: styles.label } })}
+
+ )
+ }
+}
+
+Loader.create = createShorthandFactory(Loader, 'content')
+
+export default Loader
diff --git a/src/index.ts b/src/index.ts
index ab806b3349..1d2a538e92 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -68,6 +68,7 @@ export {
} from './components/ItemLayout/ItemLayout'
export { default as Label, LabelProps } from './components/Label/Label'
+export { default as Loader, LoaderProps } from './components/Loader/Loader'
export { default as Layout, LayoutPropsWithDefaults, LayoutProps } from './components/Layout/Layout'
diff --git a/src/lib/accessibility/Behaviors/Loader/loaderBehavior.ts b/src/lib/accessibility/Behaviors/Loader/loaderBehavior.ts
new file mode 100644
index 0000000000..c9c8fb1282
--- /dev/null
+++ b/src/lib/accessibility/Behaviors/Loader/loaderBehavior.ts
@@ -0,0 +1,19 @@
+import { Accessibility } from '../../types'
+
+/**
+ * @description
+ * Loader is usually an element that displays the progress status for a task that take a long time or consists of several steps.
+ *
+ * @specification
+ * Adds role 'progressbar' to 'root' component's part.
+ */
+
+const loaderBehavior: Accessibility = () => ({
+ attributes: {
+ root: {
+ role: 'progressbar',
+ },
+ },
+})
+
+export default loaderBehavior
diff --git a/src/lib/accessibility/index.ts b/src/lib/accessibility/index.ts
index d2cd5944bd..a7dc324f89 100644
--- a/src/lib/accessibility/index.ts
+++ b/src/lib/accessibility/index.ts
@@ -12,6 +12,7 @@ export { default as listBehavior } from './Behaviors/List/listBehavior'
export { default as listItemBehavior } from './Behaviors/List/listItemBehavior'
export { default as selectableListBehavior } from './Behaviors/List/selectableListBehavior'
export { default as selectableListItemBehavior } from './Behaviors/List/selectableListItemBehavior'
+export { default as loaderBehavior } from './Behaviors/Loader/loaderBehavior'
export { default as inputBehavior } from './Behaviors/Input/inputBehavior'
export { default as iconBehavior } from './Behaviors/Icon/iconBehavior'
export { default as tabBehavior } from './Behaviors/Tab/tabBehavior'
diff --git a/src/themes/base/componentStyles.ts b/src/themes/base/componentStyles.ts
index a15804c380..1c5c9b219a 100644
--- a/src/themes/base/componentStyles.ts
+++ b/src/themes/base/componentStyles.ts
@@ -1 +1,2 @@
+export { default as Loader } from './components/Loader/loaderStyles'
export { default as Text } from './components/Text/textStyles'
diff --git a/src/themes/base/componentVariables.ts b/src/themes/base/componentVariables.ts
index 9acfb21044..f3c7863fd4 100644
--- a/src/themes/base/componentVariables.ts
+++ b/src/themes/base/componentVariables.ts
@@ -1 +1,2 @@
+export { default as Loader } from './components/Loader/loaderVariables'
export { default as Text } from './components/Text/textVariables'
diff --git a/src/themes/base/components/Loader/loaderStyles.ts b/src/themes/base/components/Loader/loaderStyles.ts
new file mode 100644
index 0000000000..2ad77754f0
--- /dev/null
+++ b/src/themes/base/components/Loader/loaderStyles.ts
@@ -0,0 +1,64 @@
+import { FlexDirectionProperty } from 'csstype'
+
+import { pxToRem } from '../../../../lib'
+import { LoaderProps } from '../../../../components/Loader/Loader'
+import { ComponentStyleFunctionParam, ICSSInJSStyle } from '../../../types'
+import { ObjectOf } from '../../../../../types/utils'
+import { LoaderVariables } from './loaderVariables'
+
+const rootFlexDirections: ObjectOf = {
+ above: 'column-reverse',
+ below: 'column',
+ start: 'row-reverse',
+ end: 'row',
+}
+
+export default {
+ root: ({
+ props: p,
+ }: ComponentStyleFunctionParam): ICSSInJSStyle => ({
+ alignItems: 'center',
+ display: p.inline ? 'inline-flex' : 'flex',
+ justifyContent: 'center',
+ flexDirection: rootFlexDirections[p.labelPosition],
+ }),
+ indicator: ({
+ props: p,
+ theme: t,
+ variables: v,
+ }: ComponentStyleFunctionParam): ICSSInJSStyle => {
+ const animationName = t.renderer.renderKeyframe(
+ () =>
+ ({
+ from: {
+ transform: 'rotate(0deg)',
+ },
+ to: {
+ transform: 'rotate(360deg)',
+ },
+ } as any),
+ {},
+ )
+ const borderColor = `${v.foregroundColor} ${v.backgroundColor} ${v.backgroundColor}`
+
+ return {
+ animationName,
+ animationDuration: '1.3s',
+ animationIterationCount: 'infinite',
+ animationTimingFunction: 'cubic-bezier(0.53, 0.21, 0.29, 0.67)',
+
+ borderColor,
+ borderRadius: '50%',
+ borderStyle: 'solid',
+ borderWidth: v.borderSizes[p.size],
+
+ boxSizing: 'border-box',
+
+ width: v.indicatorSizes[p.size],
+ height: v.indicatorSizes[p.size],
+ }
+ },
+ label: {
+ margin: pxToRem(10),
+ },
+}
diff --git a/src/themes/base/components/Loader/loaderVariables.ts b/src/themes/base/components/Loader/loaderVariables.ts
new file mode 100644
index 0000000000..14cd2d0d81
--- /dev/null
+++ b/src/themes/base/components/Loader/loaderVariables.ts
@@ -0,0 +1,38 @@
+import { pxToRem } from '../../../../lib'
+import { LoaderSize } from '../../../../components/Loader/Loader'
+
+export interface LoaderVariables {
+ foregroundColor: string
+ backgroundColor: string
+
+ borderSizes: Record
+ indicatorSizes: Record
+}
+
+export default (siteVariables): LoaderVariables => ({
+ foregroundColor: siteVariables.colors.grey[400],
+ backgroundColor: siteVariables.colors.grey[100],
+
+ borderSizes: {
+ smallest: pxToRem(1.5),
+ small: pxToRem(2),
+ smaller: pxToRem(2),
+
+ medium: pxToRem(2),
+
+ large: pxToRem(2.5),
+ larger: pxToRem(3),
+ largest: pxToRem(4),
+ },
+ indicatorSizes: {
+ smallest: pxToRem(16),
+ small: pxToRem(20),
+ smaller: pxToRem(22),
+
+ medium: pxToRem(24),
+
+ large: pxToRem(26),
+ larger: pxToRem(30),
+ largest: pxToRem(34),
+ },
+})
diff --git a/test/specs/behaviors/behavior-test.tsx b/test/specs/behaviors/behavior-test.tsx
index f532331884..76070fd1eb 100644
--- a/test/specs/behaviors/behavior-test.tsx
+++ b/test/specs/behaviors/behavior-test.tsx
@@ -12,6 +12,7 @@ import {
iconBehavior,
imageBehavior,
inputBehavior,
+ loaderBehavior,
menuBehavior,
menuItemBehavior,
submenuBehavior,
@@ -45,6 +46,7 @@ testHelper.addBehavior('buttonBehavior', buttonBehavior)
testHelper.addBehavior('iconBehavior', iconBehavior)
testHelper.addBehavior('inputBehavior', inputBehavior)
testHelper.addBehavior('imageBehavior', imageBehavior)
+testHelper.addBehavior('loaderBehavior', loaderBehavior)
testHelper.addBehavior('menuBehavior', menuBehavior)
testHelper.addBehavior('menuItemBehavior', menuItemBehavior)
testHelper.addBehavior('submenuBehavior', submenuBehavior)
diff --git a/test/specs/components/Loader/Loader-test.tsx b/test/specs/components/Loader/Loader-test.tsx
new file mode 100644
index 0000000000..5e2d97684b
--- /dev/null
+++ b/test/specs/components/Loader/Loader-test.tsx
@@ -0,0 +1,6 @@
+import { isConformant } from 'test/specs/commonTests'
+import Loader from 'src/components/Loader/Loader'
+
+describe('Loader', () => {
+ isConformant(Loader)
+})