diff --git a/packages/pelagos/__tests__/components/ProgressBar-test.js b/packages/pelagos/__tests__/components/ProgressBar-test.js
new file mode 100644
index 00000000..059abe39
--- /dev/null
+++ b/packages/pelagos/__tests__/components/ProgressBar-test.js
@@ -0,0 +1,40 @@
+import {shallow} from 'enzyme';
+
+import ProgressBar from '../../src/components/ProgressBar';
+import useRandomId from '../../src/hooks/useRandomId';
+
+jest.unmock('../../src/components/ProgressBar');
+
+useRandomId.mockReturnValue('random-id');
+
+describe('ProgressBar', () => {
+ describe('rendering', () => {
+ it('renders expected elements', () => {
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ it('renders expected elements when optional properties are set', () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.getElement()).toMatchSnapshot();
+ expect(useRandomId.mock.calls).toEqual([['test']]);
+ });
+
+ it('renders expected elements when value is not set', () => {
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ it('renders expected elements when status is finished', () => {
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+
+ it('renders expected elements when status is error', () => {
+ const wrapper = shallow();
+ expect(wrapper.getElement()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/pelagos/__tests__/components/__snapshots__/ProgressBar-test.js.snap b/packages/pelagos/__tests__/components/__snapshots__/ProgressBar-test.js.snap
new file mode 100644
index 00000000..b1805e15
--- /dev/null
+++ b/packages/pelagos/__tests__/components/__snapshots__/ProgressBar-test.js.snap
@@ -0,0 +1,196 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProgressBar rendering renders expected elements 1`] = `
+
+
+
+ Test label
+
+
+
+
+
+
+`;
+
+exports[`ProgressBar rendering renders expected elements when optional properties are set 1`] = `
+
+
+
+ Test label
+
+
+
+
+
+
+ Test helper
+
+
+`;
+
+exports[`ProgressBar rendering renders expected elements when status is error 1`] = `
+
+
+
+ Test label
+
+
+
+
+
+
+
+`;
+
+exports[`ProgressBar rendering renders expected elements when status is finished 1`] = `
+
+
+
+ Test label
+
+
+
+
+
+
+
+`;
+
+exports[`ProgressBar rendering renders expected elements when value is not set 1`] = `
+
+
+
+ Test label
+
+
+
+
+
+
+`;
diff --git a/packages/pelagos/src/components/ProgressBar.js b/packages/pelagos/src/components/ProgressBar.js
new file mode 100644
index 00000000..c6450bcc
--- /dev/null
+++ b/packages/pelagos/src/components/ProgressBar.js
@@ -0,0 +1,80 @@
+import PropTypes from 'prop-types';
+import {faCircleCheck} from '@fortawesome/free-solid-svg-icons';
+
+import useRandomId from '../hooks/useRandomId';
+import rhombusExclamation from '../icons/rhombusExclamation';
+
+import SvgIcon from './SvgIcon';
+import Layer from './Layer';
+
+import './ProgressBar.less';
+
+const icons = {finished: faCircleCheck, error: rhombusExclamation};
+
+/** Displays the progress status for tasks that take a long time. */
+const ProgressBar = ({id, className, label, helperText, type, size, status, max, value}) => {
+ id = useRandomId(id);
+ const labelId = `${id}-label`;
+ const helperId = `${id}-helper`;
+ if (status === 'active' && (value === null || value === undefined)) {
+ status = 'indeterminate';
+ }
+ return (
+
+
+ {label}
+ {status in icons && }
+
+
+
+
+ {helperText && (
+
+ {helperText}
+
+ )}
+
+ );
+};
+
+ProgressBar.propTypes = {
+ /** The component id. */
+ id: PropTypes.string,
+ /** The component class name(s). */
+ className: PropTypes.string,
+ /** The label text. */
+ label: PropTypes.string,
+ /** The textual representation of the current progress. */
+ helperText: PropTypes.string,
+ /** The alignment type. */
+ type: PropTypes.oneOf(['default', 'inline', 'indented']),
+ /** The size of the progress bar. */
+ size: PropTypes.oneOf(['small', 'big']),
+ /** The progress bar status. */
+ status: PropTypes.oneOf(['active', 'finished', 'error']),
+ /** The maximum value. */
+ max: PropTypes.number,
+ /** The current value. */
+ value: PropTypes.number,
+};
+
+ProgressBar.defaultProps = {
+ type: 'default',
+ size: 'big',
+ status: 'active',
+ max: 100,
+};
+
+export default ProgressBar;
diff --git a/packages/pelagos/src/components/ProgressBar.less b/packages/pelagos/src/components/ProgressBar.less
new file mode 100644
index 00000000..a816e79f
--- /dev/null
+++ b/packages/pelagos/src/components/ProgressBar.less
@@ -0,0 +1,138 @@
+@import '../../less/fonts';
+@import '../../less/spacing';
+@import '../../less/utils';
+
+@layer pelagos {
+ .ProgressBar {
+ gap: @sp-08;
+
+ &__label {
+ @label-01();
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: @sp-16;
+
+ .ProgressBar--inline > & {
+ justify-content: flex-start;
+ }
+
+ .ProgressBar--indented > & {
+ margin-left: @sp-16;
+ }
+ }
+
+ &__labelText {
+ @base-ellipsis();
+ }
+
+ &__icon {
+ font-size: 16px;
+
+ .ProgressBar--finished & {
+ color: var(--support-success);
+ }
+
+ .ProgressBar--error & {
+ color: var(--support-error);
+ }
+ }
+
+ &__track {
+ position: relative;
+ flex-direction: row;
+ background-color: var(--layer-accent);
+ overflow: hidden;
+
+ .ProgressBar--inline > & {
+ flex: 1;
+ }
+
+ .ProgressBar--big > & {
+ height: 8px;
+ }
+
+ .ProgressBar--small > & {
+ height: 4px;
+ }
+
+ .ProgressBar--indeterminate > &::after {
+ content: '';
+ position: absolute;
+ width: 25%;
+ height: 100%;
+ background-color: var(--interactive);
+ animation: progress-bar-indeterminate 1.5s linear infinite;
+ will-change: transform;
+
+ @media (prefers-reduced-motion) {
+ animation-duration: 6s;
+ }
+ }
+
+ // stylelint-disable-next-line @bluecateng/selector-bem -- currently doesn't support :is
+ .ProgressBar--inline:is(.ProgressBar--finished, .ProgressBar--error) > & {
+ position: absolute;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ }
+ }
+
+ &__bar {
+ width: 100%;
+ transform: scaleX(0);
+ transform-origin: 0;
+ transition: transform 0.15s ease-out;
+
+ .ProgressBar--active & {
+ background-color: var(--interactive);
+ }
+
+ .ProgressBar--finished & {
+ background-color: var(--support-success);
+ transform: scaleX(1);
+ }
+
+ .ProgressBar--error & {
+ background-color: var(--support-error);
+ transform: scaleX(1);
+ }
+ }
+
+ &__helper {
+ @helper-text-01();
+ color: var(--text-helper);
+
+ .ProgressBar--inline > & {
+ position: absolute;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ }
+
+ .ProgressBar--indented > & {
+ margin-left: @sp-16;
+ }
+
+ .ProgressBar--error > & {
+ color: var(--support-error);
+ }
+ }
+
+ &--inline {
+ flex-direction: row;
+ align-items: center;
+ gap: @sp-16;
+ }
+ }
+
+ @keyframes progress-bar-indeterminate {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(400%);
+ }
+ }
+}
diff --git a/packages/pelagos/src/components/ProgressBar.stories.js b/packages/pelagos/src/components/ProgressBar.stories.js
new file mode 100644
index 00000000..cec0f30f
--- /dev/null
+++ b/packages/pelagos/src/components/ProgressBar.stories.js
@@ -0,0 +1,51 @@
+import WithLayers from '../../templates/WithLayers';
+
+import ProgressBar from './ProgressBar';
+
+export default {
+ title: 'Components/ProgressBar',
+ component: ProgressBar,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export const Default = {
+ args: {label: 'Default', helperText: 'Some helper text', type: 'default', size: 'big', status: 'active', value: 20},
+};
+
+export const Inline = {
+ args: {...Default.args, label: 'Inline', type: 'inline'},
+};
+
+export const Indented = {
+ args: {...Default.args, label: 'Indented', type: 'indented'},
+};
+
+export const Small = {
+ args: {...Default.args, label: 'Small', size: 'small'},
+};
+
+export const Indeterminate = {
+ args: {...Default.args, label: 'Indeterminate', value: null},
+ parameters: {chromatic: {disableSnapshot: true}}, // avoid false positives due to the animation
+};
+
+export const Finished = {
+ args: {...Default.args, label: 'Finished', status: 'finished'},
+};
+
+export const Error = {
+ args: {...Default.args, label: 'Error', status: 'error'},
+};
+
+export const _WithLayers = {
+ render: () => {() => },
+ parameters: {
+ controls: {hideNoControlsWarning: true},
+ },
+};
diff --git a/packages/pelagos/src/components/index.js b/packages/pelagos/src/components/index.js
index b3465c04..fce58db4 100644
--- a/packages/pelagos/src/components/index.js
+++ b/packages/pelagos/src/components/index.js
@@ -34,6 +34,7 @@ export {default as MenuArrow} from './MenuArrow';
export {default as ModalSpinner} from './ModalSpinner';
export {default as MultiColumn} from './MultiColumn';
export {default as Pagination} from './Pagination';
+export {default as ProgressBar} from './ProgressBar';
export {default as RadioButton} from './RadioButton';
export {default as RadioGroup} from './RadioGroup';
export {default as ScrollBox} from './ScrollBox';