Feedback

Progress Circle

Circular progress indicator built on react-aria-components, styled with the quebi design system. Shows determinate value as a brand-teal ring over a faint cyan track, or spins continuously when indeterminate.

progressloadingspinnerfeedbackcircular

Values

Determinate progress at several fill levels.

Indeterminate

Continuous spin for unknown-duration work.

Inline

Inherits the surrounding font size next to text.

Saving changes…

Animated

A value that climbs from 0 to 100 and loops.

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { ProgressBar, type ProgressBarProps } from "react-aria-components"
import { cn } from "@/lib/utils"

/**
 * ProgressCircle — quebi design system
 *
 * Built on react-aria-components. A circular progress indicator: a faint
 * cyan track with a brand-teal ring that fills clockwise as the value grows.
 * Pass `isIndeterminate` for a continuous spinning state (useful as a button
 * or inline loading glyph). Inherits its size from the surrounding font size,
 * or override with a `size-*` class.
 */
interface ProgressCircleProps extends Omit<ProgressBarProps, "className"> {
  className?: string
  ref?: React.RefObject<HTMLDivElement>
}

function ProgressCircle({ className, ref, ...props }: ProgressCircleProps) {
  const c = "50%"
  const r = "calc(50% - 2px)"
  return (
    <ProgressBar {...props} ref={ref}>
      {({ percentage, isIndeterminate }) => (
        <svg
          aria-hidden="true"
          className={cn("size-4 shrink-0", className)}
          viewBox="0 0 24 24"
          fill="none"
          data-slot="icon"
        >
          <circle
            cx={c}
            cy={c}
            r={r}
            strokeWidth={3}
            className="stroke-cyan-500/10"
          />
          {!isIndeterminate ? (
            <circle
              cx={c}
              cy={c}
              r={r}
              strokeWidth={3}
              pathLength={100}
              strokeDasharray="100 200"
              strokeDashoffset={100 - (percentage ?? 0)}
              strokeLinecap="round"
              transform="rotate(-90)"
              className="origin-center stroke-quebi-brand transition-[stroke-dashoffset] duration-200"
            />
          ) : (
            <circle
              cx={c}
              cy={c}
              r={r}
              strokeWidth={3}
              pathLength={100}
              strokeDasharray="100 200"
              strokeDashoffset={100 - 30}
              strokeLinecap="round"
              className="origin-center stroke-quebi-brand animate-[spin_1s_cubic-bezier(0.4,0,0.2,1)_infinite]"
            />
          )}
        </svg>
      )}
    </ProgressBar>
  )
}

export type { ProgressCircleProps }
export { ProgressCircle }