diff --git a/packages/react/src/components/progress-circular/progress-circular.test.tsx b/packages/react/src/components/progress-circular/progress-circular.test.tsx
new file mode 100644
index 000000000..d49e16381
--- /dev/null
+++ b/packages/react/src/components/progress-circular/progress-circular.test.tsx
@@ -0,0 +1,12 @@
+import { renderWithTheme } from '../../test-utils/renderer';
+import { ProgressCircular } from './progress-circular';
+
+describe('ProgressCircular', () => {
+ test('Matches the snapshot', () => {
+ const tree = renderWithTheme(
+ ,
+ );
+
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/packages/react/src/components/progress-circular/progress-circular.test.tsx.snap b/packages/react/src/components/progress-circular/progress-circular.test.tsx.snap
new file mode 100644
index 000000000..08981d646
--- /dev/null
+++ b/packages/react/src/components/progress-circular/progress-circular.test.tsx.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProgressCircular Matches the snapshot 1`] = `
+.c0 {
+ -webkit-transition: stroke-dashoffset 850ms ease;
+ transition: stroke-dashoffset 850ms ease;
+}
+
+
+`;
diff --git a/packages/react/src/components/progress-circular/progress-circular.tsx b/packages/react/src/components/progress-circular/progress-circular.tsx
new file mode 100644
index 000000000..91606ed4b
--- /dev/null
+++ b/packages/react/src/components/progress-circular/progress-circular.tsx
@@ -0,0 +1,65 @@
+import { VoidFunctionComponent } from 'react';
+import styled, { useTheme } from 'styled-components';
+
+const sizes = {
+ xsmall: 16,
+ small: 24,
+ medium: 32,
+ large: 64,
+};
+
+type Size = keyof typeof sizes;
+
+const VIEWBOX = 64;
+const STROKE_WIDTH = 8;
+const RADIUS = (VIEWBOX - STROKE_WIDTH) / 2;
+const CENTER_XY = VIEWBOX / 2;
+const CIRCUMFERENCE = RADIUS * 2 * Math.PI;
+
+const Circle = styled.circle`
+ transition: stroke-dashoffset 850ms ease;
+`;
+
+export interface ProgressCircularProps {
+ className?: string;
+ size?: Size;
+ inverted?: boolean;
+ value: number;
+}
+
+// Source: https://css-tricks.com/building-progress-ring-quickly/
+export const ProgressCircular: VoidFunctionComponent = ({
+ className,
+ inverted = false,
+ size = 'medium',
+ value,
+}) => {
+ const theme = useTheme();
+ const strokeDashoffset = (1 - (value / 100)) * CIRCUMFERENCE;
+
+ return (
+
+ );
+};
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 0f316e27a..5ffd403d8 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -25,6 +25,7 @@ export { NumericInput } from './components/numeric-input/numeric-input';
export { PasswordCreationInput } from './components/password-creation-input/password-creation-input';
export { PasswordInput } from './components/password-input/password-input';
export { PhoneInput } from './components/phone-input/phone-input';
+export { ProgressCircular, ProgressCircularProps } from './components/progress-circular/progress-circular';
export { RadioButtonGroup } from './components/radio-button-group/radio-button-group';
export { SearchContextual } from './components/search/search-contextual';
export { SearchGlobal } from './components/search/search-global';
diff --git a/packages/react/src/themes/tokens/component-tokens.ts b/packages/react/src/themes/tokens/component-tokens.ts
index 13bd12442..3e27e3aa4 100644
--- a/packages/react/src/themes/tokens/component-tokens.ts
+++ b/packages/react/src/themes/tokens/component-tokens.ts
@@ -25,6 +25,7 @@ import { defaultMenuTokens, MenuTokens } from './component/menu-tokens';
import { defaultNumericInputTokens, NumericInputTokens } from './component/numeric-input-tokens';
import { defaultPasswordInputTokens, PasswordInputTokens } from './component/password-input-tokens';
import { defaultPhoneInputTokens, PhoneInputTokens } from './component/phone-input-tokens';
+import { defaultProgressCircularTokens, ProgressCircularTokens } from './component/progress-circular-tokens';
import { defaultRadioButtonGroupTokens, RadioButtonGroupTokens } from './component/radio-button-group-tokens';
import { defaultRadioCardTokens, RadioCardTokens } from './component/radio-card-tokens';
import { defaultSearchInputTokens, SearchInputTokens } from './component/search-input-tokens';
@@ -85,6 +86,7 @@ export type ComponentTokens =
| NavListTokens
| PaginationTokens
| ProgressTokens
+ | ProgressCircularTokens
| LinkTokens
| BadgeTokens
| GlobalBannerTokens
@@ -125,6 +127,7 @@ export const defaultComponentTokens: ComponentTokenMap = {
...defaultNavListTokens,
...defaultPaginationTokens,
...defaultProgressTokens,
+ ...defaultProgressCircularTokens,
...defaultLinkTokens,
...defaultCheckboxTokens,
...defaultChooserTokens,
diff --git a/packages/react/src/themes/tokens/component/progress-circular-tokens.ts b/packages/react/src/themes/tokens/component/progress-circular-tokens.ts
new file mode 100644
index 000000000..c5028b02c
--- /dev/null
+++ b/packages/react/src/themes/tokens/component/progress-circular-tokens.ts
@@ -0,0 +1,17 @@
+import { AliasTokens } from '../alias-tokens';
+import { RefTokens } from '../ref-tokens';
+
+export type ProgressCircularTokens =
+ | 'progress-circular-color'
+ | 'progress-circular-inverted-color'
+
+export type ProgressCircularTokenValue = AliasTokens | RefTokens;
+
+export type ProgressCircularTokenMap = {
+ [Token in ProgressCircularTokens]: ProgressCircularTokenValue;
+};
+
+export const defaultProgressCircularTokens: ProgressCircularTokenMap = {
+ 'progress-circular-color': 'color-background-brand',
+ 'progress-circular-inverted-color': 'color-white',
+};
diff --git a/packages/storybook/stories/progress-circular.stories.tsx b/packages/storybook/stories/progress-circular.stories.tsx
new file mode 100644
index 000000000..0db84c09c
--- /dev/null
+++ b/packages/storybook/stories/progress-circular.stories.tsx
@@ -0,0 +1,47 @@
+import { ProgressCircular } from '@equisoft/design-elements-react';
+import { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ title: 'Components/Progress Circular',
+ component: ProgressCircular,
+ args: {
+ size: 'medium',
+ inverted: false,
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ },
+ inverted: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ value: {
+ control: {
+ type: 'range',
+ min: 0,
+ max: 100,
+ },
+ },
+ size: {
+ control: {
+ type: 'select',
+ options: ['xsmall', 'small', 'medium', 'large'],
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ value: 60,
+ inverted: false,
+ },
+};