# ProgressCircle

**📖 Live documentation:** https://cds.coinbase.com/components/feedback/ProgressCircle/

A circular visual indicator of completion progress. Supports both determinate progress (0–100%) and an indeterminate variant for loading states.

## Import

```tsx
import { ProgressCircle } from '@coinbase/cds-web/visualizations/ProgressCircle'
```

## Examples

### Default

```jsx live
<HStack gap={2} flexWrap="wrap">
  <ProgressCircle progress={0} size={100} />
  <ProgressCircle progress={0.5} size={100} />
  <ProgressCircle progress={1} size={100} />
</HStack>
```

### Indeterminate

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.

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.

#### Thickness (weight)

Indeterminate uses the same `weight` prop as determinate progress. The default is **`"normal"`** (4px stroke). Use `"thin"` (2px), `"semiheavy"` (8px), or `"heavy"` (12px) to change thickness.

```jsx live
<HStack gap={2} flexWrap="wrap" alignItems="center">
  <VStack gap={1} alignItems="center">
    <Text variant="label2">Default (normal)</Text>
    <ProgressCircle accessibilityLabel="Loading" indeterminate size={64} />
  </VStack>
  <VStack gap={1} alignItems="center">
    <Text variant="label2">weight="thin"</Text>
    <ProgressCircle accessibilityLabel="Loading" indeterminate size={64} weight="thin" />
  </VStack>
  <VStack gap={1} alignItems="center">
    <Text variant="label2">weight="semiheavy"</Text>
    <ProgressCircle accessibilityLabel="Loading" indeterminate size={64} weight="semiheavy" />
  </VStack>
</HStack>
```

#### Progress (arc length)

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.

```jsx live
<HStack gap={2} flexWrap="wrap" alignItems="center">
  <VStack gap={1} alignItems="center">
    <Text variant="label2">progress=0.25</Text>
    <ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.25} size={56} />
  </VStack>
  <VStack gap={1} alignItems="center">
    <Text variant="label2">progress=0.5</Text>
    <ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.5} size={56} />
  </VStack>
  <VStack gap={1} alignItems="center">
    <Text variant="label2">progress=0.75 (default)</Text>
    <ProgressCircle accessibilityLabel="Loading" indeterminate progress={0.75} size={56} />
  </VStack>
</HStack>
```

#### Sizes and color

```jsx live
<HStack gap={2} flexWrap="wrap" alignItems="center">
  <ProgressCircle accessibilityLabel="Loading" indeterminate size={32} />
  <ProgressCircle accessibilityLabel="Loading" indeterminate size={56} />
  <ProgressCircle accessibilityLabel="Loading" indeterminate size={100} />
  <ProgressCircle accessibilityLabel="Loading" indeterminate color="bgPrimary" size={56} />
</HStack>
```

### Thin

```jsx live
<HStack gap={2} flexWrap="wrap">
  <ProgressCircle progress={0} weight="thin" size={100} />
  <ProgressCircle progress={0.5} weight="thin" size={100} />
  <ProgressCircle progress={1} weight="thin" size={100} />
</HStack>
```

### Semiheavy

```jsx live
<HStack gap={2} flexWrap="wrap">
  <ProgressCircle progress={0} weight="semiheavy" size={100} />
  <ProgressCircle progress={0.5} weight="semiheavy" size={100} />
  <ProgressCircle progress={1} weight="semiheavy" size={100} />
</HStack>
```

### Heavy

```jsx live
<HStack gap={2} flexWrap="wrap">
  <ProgressCircle progress={0} weight="heavy" size={100} />
  <ProgressCircle progress={0.5} weight="heavy" size={100} />
  <ProgressCircle progress={1} weight="heavy" size={100} />
</HStack>
```

### No Text

```jsx live
<HStack gap={2} flexWrap="wrap">
  <ProgressCircle progress={0} hideContent size={25} />
  <ProgressCircle progress={0.5} hideContent size={25} />
  <ProgressCircle progress={1} hideContent size={25} />
</HStack>
```

### Disabled

```jsx live
<HStack gap={2} flexWrap="wrap">
  <ProgressCircle progress={0} disabled size={100} />
  <ProgressCircle progress={0.5} disabled size={100} />
  <ProgressCircle progress={1} disabled size={100} />
</HStack>
```

### Colors

```jsx live
<HStack gap={2} flexWrap="wrap">
  <ProgressCircle progress={0.5} color="bgPositive" size={100} />
  <ProgressCircle progress={0.5} color="bgNegative" size={100} />
  <ProgressCircle progress={0.5} color="bgPrimary" size={100} />
  <ProgressCircle progress={0.5} color="fg" size={100} />
</HStack>
```

### Fill Parent

The progress circle can be dynamically sized to fit its parent. If you drag the browser window smaller or larger then the ProgressCircle will resize accordingly.

```jsx live
<HStack gap={2} flexWrap="wrap">
  <div style={{ height: '15vw', width: '15vw', minWidth: '60px', minHeight: '60px' }}>
    <ProgressCircle progress={1} />
  </div>
  <div style={{ height: '10vw', width: '10vw', minWidth: '60px', minHeight: '60px' }}>
    <ProgressCircle progress={1} />
  </div>
  <div style={{ height: '5vw', width: '5vw', minWidth: '60px', minHeight: '60px' }}>
    <ProgressCircle progress={1} />
  </div>
</HStack>
```

### Content Node Customization

You can override the default content node to display a custom node. Note that the content node is clipped to the circle.

#### With Asset

You can provide an image, such as an asset, as the content node.

```jsx live
<VStack gap={2}>
  <HStack gap={2} flexWrap="wrap">
    <ProgressCircle
      progress={1}
      size={56}
      styles={{
        progress: {
          stroke: assets.eth.color,
        },
      }}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.eth.name}
            shape="circle"
            source={assets.eth.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />

    <ProgressCircle
      progress={0.75}
      size={56}
      styles={{
        progress: {
          stroke: assets.ltc.color,
        },
      }}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.ltc.name}
            shape="circle"
            source={assets.ltc.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
    <ProgressCircle
      progress={0.5}
      size={56}
      styles={{
        progress: {
          stroke: assets.dai.color,
        },
      }}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            shape="circle"
            source={assets.dai.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
    <ProgressCircle
      progress={0.25}
      size={56}
      styles={{
        progress: {
          stroke: assets.sushi.color,
        },
      }}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.sushi.name}
            shape="circle"
            source={assets.sushi.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
    <ProgressCircle
      progress={0}
      size={56}
      styles={{
        progress: {
          stroke: assets.xrp.color,
        },
      }}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.xrp.name}
            shape="circle"
            source={assets.xrp.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
  </HStack>
  <HStack gap={2} flexWrap="wrap">
    <ProgressCircle
      styles={{
        progress: {
          stroke: assets.btc.color,
        },
      }}
      progress={0.24}
      size={24}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.btc.name}
            shape="circle"
            source={assets.btc.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
    <ProgressCircle
      styles={{
        progress: {
          stroke: assets.btc.color,
        },
      }}
      progress={0.24}
      size={32}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.btc.name}
            shape="circle"
            source={assets.btc.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
    <ProgressCircle
      styles={{
        progress: {
          stroke: assets.btc.color,
        },
      }}
      progress={0.24}
      size={40}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.btc.name}
            shape="circle"
            source={assets.btc.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
    <ProgressCircle
      styles={{
        progress: {
          stroke: assets.btc.color,
        },
      }}
      progress={0.24}
      size={48}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.btc.name}
            shape="circle"
            source={assets.btc.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
    <ProgressCircle
      styles={{
        progress: {
          stroke: assets.btc.color,
        },
      }}
      progress={0.24}
      size={56}
      contentNode={
        <Box height="100%" padding={0.25} width="100%">
          <RemoteImage
            alt={assets.btc.name}
            shape="circle"
            source={assets.btc.imageUrl}
            style={{ width: '100%', height: '100%' }}
          />
        </Box>
      }
      weight="thin"
    />
  </HStack>
</VStack>
```

#### Custom Text Color

The progress circle's default content can be customized to display a custom text color.

```jsx live
<HStack gap={2}>
  <ProgressCircle
    color="fgPrimary"
    progress={0.2}
    size={100}
    contentNode={<DefaultProgressCircleContent color="fgPrimary" progress={0.2} />}
  />
  <ProgressCircle
    color="fgPositive"
    progress={0.4}
    size={100}
    contentNode={<DefaultProgressCircleContent color="fgPositive" progress={0.4} />}
  />
</HStack>
```

### Custom Styles

The progress circle can be customized with styles and class names.

```jsx live
<HStack gap={2}>
  <ProgressCircle
    progress={0.4}
    size={100}
    styles={{
      circle: {
        stroke: 'transparent',
      },
    }}
    contentNode={
      <Text font="title1" color="fgPrimary">
        40%
      </Text>
    }
    weight="semiheavy"
  />
  <ProgressCircle
    color="fgPositive"
    progress={0.6}
    size={100}
    styles={{
      progress: {
        strokeLinecap: 'square',
      },
    }}
    contentNode={<Icon color="fgPositive" name="circleCheckmark" size="l" />}
  />
</HStack>
```

### Interactive Demo

This is for demo purposes. ProgressContainerWithButtons isn't designed for production usage.

```jsx live
<ProgressContainerWithButtons>
  {({ calculateProgress }) => (
    <HStack gap={2}>
      <ProgressCircle progress={calculateProgress(0)} size={100} />
      <ProgressCircle progress={calculateProgress(0.2)} size={100} />
    </HStack>
  )}
</ProgressContainerWithButtons>
```

### Animation

By default, ProgressCircle animates progress changes. Use `disableAnimateOnMount` to skip the initial animation while still animating subsequent changes.

```tsx live
<ProgressContainerWithButtons>
  {({ calculateProgress }) => (
    <HStack gap={2}>
      <VStack gap={1} alignItems="center">
        <Text variant="label2">Normal animation</Text>
        <ProgressCircle progress={calculateProgress(0)} size={100} />
      </VStack>

      <VStack gap={1} alignItems="center">
        <Text variant="label2">Disable animation on mount</Text>
        <ProgressCircle disableAnimateOnMount progress={calculateProgress(0.3)} size={100} />
      </VStack>
    </HStack>
  )}
</ProgressContainerWithButtons>
```

#### Callbacks

You can use the `onAnimationStart` and `onAnimationEnd` callbacks to track the progress of the animation.

```jsx live
function Example() {
  const [animationStatus, setAnimationStatus] = React.useState('Ready');

  const handleAnimationStart = useCallback(() => {
    setAnimationStatus('Animating...');
  }, []);

  const handleAnimationEnd = useCallback(() => {
    setAnimationStatus('Animation Ended');
  }, []);

  return (
    <ProgressContainerWithButtons>
      {({ calculateProgress }) => (
        <VStack gap={2}>
          <Text>Animation Status: {animationStatus}</Text>
          <ProgressCircle
            onAnimationEnd={handleAnimationEnd}
            onAnimationStart={handleAnimationStart}
            progress={calculateProgress(0.2)}
            size={100}
          />
        </VStack>
      )}
    </ProgressContainerWithButtons>
  );
}
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | No | `-` | - |
| `classNames` | `{ root?: string; svg?: string \| undefined; circle?: string \| undefined; progress?: string \| undefined; } \| undefined` | No | `-` | Custom class names for individual elements of the ProgressCircle component |
| `color` | `currentColor \| fg \| fgMuted \| fgInverse \| fgPrimary \| fgWarning \| fgPositive \| fgNegative \| bg \| bgAlternate \| bgInverse \| bgOverlay \| bgElevation1 \| bgElevation2 \| bgPrimary \| bgPrimaryWash \| bgSecondary \| bgTertiary \| bgSecondaryWash \| bgNegative \| bgNegativeWash \| bgPositive \| bgPositiveWash \| bgWarning \| bgWarningWash \| bgLine \| bgLineHeavy \| bgLineInverse \| bgLinePrimary \| bgLinePrimarySubtle \| accentSubtleRed \| accentBoldRed \| accentSubtleGreen \| accentBoldGreen \| accentSubtleBlue \| accentBoldBlue \| accentSubtlePurple \| accentBoldPurple \| accentSubtleYellow \| accentBoldYellow \| accentSubtleGray \| accentBoldGray \| transparent` | No | `primary` | Custom progress color. |
| `contentNode` | `null \| string \| number \| bigint \| false \| true \| ReactElement<unknown, string \| JSXElementConstructor<any>> \| Iterable<ReactNode> \| ReactPortal \| Promise<AwaitedReactNode>` | No | `-` | Optional component to override the default content rendered inside the circle. |
| `disableAnimateOnMount` | `boolean` | No | `false` | Disable animation on component mount |
| `disabled` | `boolean` | No | `-` | Toggle used to show a disabled progress visualization |
| `hideContent` | `boolean` | No | `-` | Toggle used to hide the content node rendered inside the circle. |
| `hideText` | `boolean` | No | `-` | - |
| `indeterminate` | `boolean` | No | `-` | Toggle used to show an indeterminate progress circle. |
| `key` | `Key \| null` | No | `-` | - |
| `onAnimationEnd` | `(() => void)` | No | `-` | Callback fired when the progress animation ends. |
| `onAnimationStart` | `(() => void)` | No | `-` | Callback fired when the progress animation starts. |
| `progress` | `number` | No | `-` | Number between 0-1 representing the progress percentage |
| `ref` | `null \| RefObject<HTMLButtonElement \| null> \| (instance: HTMLButtonElement \| null) => void \| (() => VoidOrUndefinedOnly)` | No | `-` | Allows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). |
| `size` | `number` | No | `-` | Optional size in px for the visualization. This is useful if the visualization is used in an HStack. If this is omitted the visualization will fill the parent width or height. Since its a circular visualization it will fill the smaller of the parent width or height |
| `style` | `CSSProperties` | No | `-` | - |
| `styles` | `{ root?: CSSProperties; svg?: CSSProperties \| undefined; circle?: CSSProperties \| undefined; progress?: CSSProperties \| undefined; } \| undefined` | No | `-` | Custom styles for individual elements of the ProgressCircle component |
| `testID` | `string` | No | `-` | Used to locate this element in unit and end-to-end tests. Under the hood, testID translates to data-testid on Web. On Mobile, testID stays the same - testID |
| `weight` | `normal \| heavy \| thin \| semiheavy` | No | `normal` | Toggle used to change thickness of progress visualization |


## Styles

| Selector | Static class name | Description |
| --- | --- | --- |
| `root` | `-` | Root element |
| `svg` | `-` | SVG element |
| `circle` | `-` | Background circle element |
| `progress` | `-` | Foreground progress circle element |


