Skip to content

Commit 1b026ee

Browse files
feat: add indeterminate ProgressCircle, unify progress size and float-label math
- Add indeterminate prop to ProgressCircle (web & mobile): spinning state with configurable weight, default stroke ratio 0.11; animate full SVG on mobile, CSS keyframes on web; hide default content when indeterminate. - Add getProgressSize(weight) in common and deprecate useProgressSize; use getProgressSize in ProgressBar and ProgressCircle on both platforms. - Simplify ProgressBarWithFloatLabel (web & mobile): remove usePreviousValues, useIsoEffect, and imperative animation; use shared getEndTranslateX so float label trailing edge follows fill end (containerWidth * progress - textWidth); web uses useMotionProps + MotionBox, mobile animates translateX to target. - ProgressBar/ProgressCircle: progress optional with default 0; add originX/ originY in getProgressCircleParams; web ProgressCircle uses pathLength=1. - Deprecate Spinner (web & mobile) in favor of indeterminate ProgressCircle. - ProgressBar tests: update float-label position expectation (80) and accept transform none/translateX(0) for zero progress; iconSvgMap regenerated.
1 parent 62beb56 commit 1b026ee

40 files changed

Lines changed: 828 additions & 320 deletions

apps/docs/docs/components/feedback/ProgressCircle/_mobileExamples.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,65 @@
88
</HStack>
99
```
1010

11+
## Indeterminate
12+
13+
Use the `indeterminate` prop when progress is unknown (e.g. loading). The circle shows a spinning partial arc with no percentage text. This is the recommended replacement for the deprecated [Spinner](/components/feedback/Spinner) in loading contexts such as [IconButton](/components/buttons/IconButton) or button loading states.
14+
15+
When `indeterminate` is true, the default color is `fgMuted`; you can override `color` as needed. Always provide `accessibilityLabel` so screen readers announce the loading state.
16+
17+
### Thickness (weight)
18+
19+
By default, the indeterminate variant uses a **stroke width of 11% of the circle size**—so thickness scales with `size` and matches the legacy Spinner look. To use a fixed stroke width instead, pass the `weight` prop: `"thin"` (2px), `"normal"` (4px), `"semiheavy"` (8px), or `"heavy"` (12px).
20+
21+
```jsx
22+
<HStack gap={2} alignItems="center">
23+
<VStack gap={1} alignItems="center">
24+
<Text variant="label2">Default (11% of size)</Text>
25+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={64} />
26+
</VStack>
27+
<VStack gap={1} alignItems="center">
28+
<Text variant="label2">weight="thin"</Text>
29+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={64} weight="thin" />
30+
</VStack>
31+
<VStack gap={1} alignItems="center">
32+
<Text variant="label2">weight="semiheavy"</Text>
33+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={64} weight="semiheavy" />
34+
</VStack>
35+
</HStack>
36+
```
37+
38+
### Progress (arc length)
39+
40+
When `indeterminate` is true, the **`progress` prop controls the length of the visible arc** (how much of the circle is drawn), not a completion percentage. It defaults to `0.75` (a 270° arc). Override it to change the arc length—e.g. `0.5` for a half circle or `0.25` for a shorter arc.
41+
42+
```jsx
43+
<HStack gap={2} alignItems="center">
44+
<VStack gap={1} alignItems="center">
45+
<Text variant="label2">progress=0.25</Text>
46+
<ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.25} size={56} />
47+
</VStack>
48+
<VStack gap={1} alignItems="center">
49+
<Text variant="label2">progress=0.5</Text>
50+
<ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.5} size={56} />
51+
</VStack>
52+
<VStack gap={1} alignItems="center">
53+
<Text variant="label2">progress=0.75 (default)</Text>
54+
<ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.75} size={56} />
55+
</VStack>
56+
</HStack>
57+
```
58+
59+
### Sizes and color
60+
61+
```jsx
62+
<HStack gap={2} alignItems="center">
63+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={32} />
64+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={56} />
65+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={100} />
66+
<ProgressCircle accessibilityLabel="Loading" indeterminate color="bgPrimary" size={56} />
67+
</HStack>
68+
```
69+
1170
## Thin
1271

1372
```jsx

apps/docs/docs/components/feedback/ProgressCircle/_webExamples.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,65 @@
88
</HStack>
99
```
1010

11+
## Indeterminate
12+
13+
Use the `indeterminate` prop when progress is unknown (e.g. loading). The circle shows a spinning partial arc with no percentage text. This is the recommended replacement for the deprecated [Spinner](/components/feedback/Spinner) in loading contexts such as [IconButton](/components/inputs/IconButton) or button loading states.
14+
15+
When `indeterminate` is true, the default color is `fgMuted`; you can override `color` as needed. Always provide `accessibilityLabel` so screen readers announce the loading state.
16+
17+
### Thickness (weight)
18+
19+
By default, the indeterminate variant uses a **stroke width of 11% of the circle size**—so thickness scales with `size` and matches the legacy Spinner look. To use a fixed stroke width instead, pass the `weight` prop: `"thin"` (2px), `"normal"` (4px), `"semiheavy"` (8px), or `"heavy"` (12px).
20+
21+
```jsx live
22+
<HStack gap={2} flexWrap="wrap" alignItems="center">
23+
<VStack gap={1} alignItems="center">
24+
<Text variant="label2">Default (11% of size)</Text>
25+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={64} />
26+
</VStack>
27+
<VStack gap={1} alignItems="center">
28+
<Text variant="label2">weight="thin"</Text>
29+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={64} weight="thin" />
30+
</VStack>
31+
<VStack gap={1} alignItems="center">
32+
<Text variant="label2">weight="semiheavy"</Text>
33+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={64} weight="semiheavy" />
34+
</VStack>
35+
</HStack>
36+
```
37+
38+
### Progress (arc length)
39+
40+
When `indeterminate` is true, the **`progress` prop controls the length of the visible arc** (how much of the circle is drawn), not a completion percentage. It defaults to `0.75` (a 270° arc). Override it to change the arc length—e.g. `0.5` for a half circle or `0.25` for a shorter arc.
41+
42+
```jsx live
43+
<HStack gap={2} flexWrap="wrap" alignItems="center">
44+
<VStack gap={1} alignItems="center">
45+
<Text variant="label2">progress=0.25</Text>
46+
<ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.25} size={56} />
47+
</VStack>
48+
<VStack gap={1} alignItems="center">
49+
<Text variant="label2">progress=0.5</Text>
50+
<ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.5} size={56} />
51+
</VStack>
52+
<VStack gap={1} alignItems="center">
53+
<Text variant="label2">progress=0.75 (default)</Text>
54+
<ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.75} size={56} />
55+
</VStack>
56+
</HStack>
57+
```
58+
59+
### Sizes and color
60+
61+
```jsx live
62+
<HStack gap={2} flexWrap="wrap" alignItems="center">
63+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={32} />
64+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={56} />
65+
<ProgressCircle accessibilityLabel="Loading" indeterminate size={100} />
66+
<ProgressCircle accessibilityLabel="Loading" indeterminate color="bgPrimary" size={56} />
67+
</HStack>
68+
```
69+
1170
## Thin
1271

1372
```jsx live

apps/docs/docs/components/feedback/ProgressCircle/mobileMetadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"import": "import { ProgressCircle } from '@coinbase/cds-mobile/visualizations/ProgressCircle'",
33
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/visualizations/ProgressCircle.tsx",
4-
"description": "A circular visual indicator of completion progress.",
4+
"description": "A circular visual indicator of completion progress. Supports both determinate progress (0–100%) and an indeterminate variant for loading states.",
55
"relatedComponents": [
66
{
77
"label": "ProgressBar",

apps/docs/docs/components/feedback/ProgressCircle/webMetadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"import": "import { ProgressCircle } from '@coinbase/cds-web/visualizations/ProgressCircle'",
33
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/visualizations/ProgressCircle.tsx",
44
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-progresscircle--default",
5-
"description": "A circular visual indicator of completion progress.",
5+
"description": "A circular visual indicator of completion progress. Supports both determinate progress (0–100%) and an indeterminate variant for loading states.",
66
"relatedComponents": [
77
{
88
"label": "ProgressBar",

apps/docs/docs/components/feedback/Spinner/mobileMetadata.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"import": "import { Spinner } from '@coinbase/cds-mobile/loaders/Spinner'",
33
"source": "https://github.com/coinbase/cds/blob/master/packages/mobile/src/loaders/Spinner.tsx",
44
"description": "A loading indicator that displays a rotating animation to communicate that content is loading or a background process is in progress.",
5+
"warning": "This component is deprecated. Use indeterminate ProgressCircle for loading indicators instead.",
56
"relatedComponents": [
67
{
78
"label": "Fallback",

apps/docs/docs/components/feedback/Spinner/webMetadata.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/loaders/Spinner.tsx",
44
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-loaders-spinner--spinner-default",
55
"description": "A loading indicator that displays a rotating animation to communicate that content is loading or a background process is in progress.",
6+
"warning": "This component is deprecated. Use indeterminate ProgressCircle for loading indicators instead.",
67
"relatedComponents": [
78
{
89
"label": "Fallback",

apps/docs/docs/components/inputs/Button/_mobileExamples.mdx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,34 @@ Use transparent buttons for supplementary actions with lower prominence. The con
5656

5757
### Loading
5858

59-
Use the `loading` prop to indicate an action is in progress. The button becomes non-interactive and displays a spinner while preserving its width.
59+
Use the `loading` prop to indicate an action is in progress. The button becomes non-interactive and displays a loading indicator (indeterminate [ProgressCircle](/components/feedback/ProgressCircle)) while preserving its width.
60+
61+
#### Loading by variant
62+
63+
Loading works with all variants and transparent. The label is hidden and the progress circle is shown in the button’s accent color.
64+
65+
```jsx
66+
<HStack gap={2} flexWrap="wrap" alignItems="center">
67+
<Button loading>Primary</Button>
68+
<Button loading variant="secondary">
69+
Secondary
70+
</Button>
71+
<Button loading variant="tertiary">
72+
Tertiary
73+
</Button>
74+
<Button loading variant="negative">
75+
Negative
76+
</Button>
77+
<Button loading transparent variant="secondary">
78+
Transparent
79+
</Button>
80+
<Button loading compact>
81+
Compact
82+
</Button>
83+
</HStack>
84+
```
85+
86+
#### Basic loading
6087

6188
```jsx
6289
<HStack gap={2}>

apps/docs/docs/components/inputs/Button/_webExamples.mdx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,36 @@ Use transparent buttons for supplementary actions with lower prominence. The con
5959

6060
### Loading
6161

62-
Use the `loading` prop to indicate an action is in progress. The button becomes non-interactive and displays a spinner while preserving its width.
62+
Use the `loading` prop to indicate an action is in progress. The button becomes non-interactive and displays a loading indicator (indeterminate [ProgressCircle](/components/feedback/ProgressCircle)) while preserving its width.
63+
64+
#### Loading by variant
65+
66+
Loading works with all variants and transparent. The label is hidden and the progress circle is shown in the button’s accent color.
67+
68+
```jsx live
69+
<HStack gap={2} flexWrap="wrap" alignItems="center">
70+
<Button loading>Primary</Button>
71+
<Button loading variant="secondary">
72+
Secondary
73+
</Button>
74+
<Button loading variant="tertiary">
75+
Tertiary
76+
</Button>
77+
<Button loading variant="negative">
78+
Negative
79+
</Button>
80+
<Button loading transparent variant="secondary">
81+
Transparent
82+
</Button>
83+
<Button loading compact>
84+
Compact
85+
</Button>
86+
</HStack>
87+
```
88+
89+
#### Interactive loading
90+
91+
Toggle loading state to see the transition. Use for async actions like save or submit.
6392

6493
```jsx live
6594
function LoadingExample() {

apps/docs/docs/components/inputs/IconButton/_webExamples.mdx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,64 @@ Use the `transparent` prop to remove the background until the user interacts wit
9090

9191
### Loading
9292

93-
Use the `loading` prop to show a spinner when an action is in progress. The button becomes non-interactive and displays a loading spinner instead of the icon.
93+
Use the `loading` prop when an action is in progress. The button becomes non-interactive and shows an indeterminate [ProgressCircle](/components/feedback/ProgressCircle) instead of the icon. The circle size follows the button’s `iconSize`.
94+
95+
#### Loading by variant
96+
97+
Loading works with all variants, transparent, and compact. Provide `accessibilityLabel` so screen readers announce the loading state (e.g. "Loading").
98+
99+
```jsx live
100+
<HStack gap={2} flexWrap="wrap" alignItems="center">
101+
<IconButton
102+
loading
103+
name="refresh"
104+
accessibilityLabel="Loading"
105+
variant="primary"
106+
onClick={console.log}
107+
/>
108+
<IconButton
109+
loading
110+
name="refresh"
111+
accessibilityLabel="Loading"
112+
variant="secondary"
113+
onClick={console.log}
114+
/>
115+
<IconButton
116+
loading
117+
name="refresh"
118+
accessibilityLabel="Loading"
119+
variant="tertiary"
120+
onClick={console.log}
121+
/>
122+
<IconButton
123+
loading
124+
name="refresh"
125+
accessibilityLabel="Loading"
126+
variant="foregroundMuted"
127+
onClick={console.log}
128+
/>
129+
<IconButton
130+
loading
131+
transparent
132+
name="refresh"
133+
accessibilityLabel="Loading"
134+
variant="secondary"
135+
onClick={console.log}
136+
/>
137+
<IconButton loading compact name="refresh" accessibilityLabel="Loading" onClick={console.log} />
138+
<IconButton
139+
loading
140+
name="refresh"
141+
accessibilityLabel="Loading"
142+
compact={false}
143+
onClick={console.log}
144+
/>
145+
</HStack>
146+
```
147+
148+
#### Interactive loading
149+
150+
Toggle loading to simulate an async action. The button’s `accessibilityLabel` can reflect the state (e.g. "Submit form" vs "Processing submission").
94151

95152
```jsx live
96153
function LoadingExample() {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { Weight } from '../types/Weight';
2+
3+
export const getProgressSize = (weight: Weight) => {
4+
switch (weight) {
5+
case 'semiheavy':
6+
return 8;
7+
case 'heavy':
8+
return 12;
9+
case 'thin':
10+
return 2;
11+
default:
12+
return 4;
13+
}
14+
};

0 commit comments

Comments
 (0)