# Circular Progress

> SVG ring progress with centered percentage, adjustable size, thickness and color.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/circular-progress
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/circular-progress.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/circular-progress-demo.tsx"
"use client"

import * as React from "react"
import { Check } from "lucide-react"

import { CircularProgress } from "@/components/circular-progress"

const btn =
  "inline-flex h-8 items-center rounded-lg border bg-background px-3 text-xs font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"

export default function CircularProgressDemo() {
  const [value, setValue] = React.useState(64)

  return (
    <div className="flex flex-col items-center gap-8">
      <div className="flex flex-wrap items-center justify-center gap-6">
        <CircularProgress value={value} size={128} thickness={10} label="Upload" />
        <CircularProgress value={value} size={88} thickness={7} color="text-emerald-500" label="Health" />
        <CircularProgress value={value} size={64} thickness={5} color="text-amber-500" label="Quota" />
        <CircularProgress value={value} size={44} thickness={4} color="text-sky-500" label="Sync" showValue={false} />
        <CircularProgress value={100} size={88} thickness={7} color="text-emerald-500" label="Done">
          <Check className="size-7 text-emerald-500" />
        </CircularProgress>
      </div>
      <div className="flex gap-2">
        <button type="button" className={btn} onClick={() => setValue((v) => Math.max(0, v - 20))}>
          -20
        </button>
        <button type="button" className={btn} onClick={() => setValue((v) => Math.min(100, v + 20))}>
          +20
        </button>
        <button type="button" className={btn} onClick={() => setValue(0)}>
          Reset
        </button>
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/circular-progress.tsx"
import * as React from "react"

import { cn } from "@/lib/utils"

export interface CircularProgressProps extends Omit<React.ComponentProps<"div">, "children"> {
  /** 0 - 100 */
  value: number
  /** Outer diameter in px */
  size?: number
  /** Stroke width in px */
  thickness?: number
  /** Tailwind text color class for the arc, e.g. "text-emerald-500". Defaults to text-primary. */
  color?: string
  /** Replace the default percent text in the center. */
  children?: React.ReactNode
  showValue?: boolean
  label?: string
}

export function CircularProgress({
  value,
  size = 96,
  thickness = 8,
  color = "text-primary",
  showValue = true,
  label = "Progress",
  className,
  children,
  style,
  ...props
}: CircularProgressProps) {
  const clamped = Math.min(Math.max(value, 0), 100)
  const radius = (size - thickness) / 2
  const circumference = 2 * Math.PI * radius
  const offset = circumference * (1 - clamped / 100)

  return (
    <div
      role="progressbar"
      aria-label={label}
      aria-valuemin={0}
      aria-valuemax={100}
      aria-valuenow={Math.round(clamped)}
      className={cn("relative inline-flex shrink-0 items-center justify-center", className)}
      style={{ width: size, height: size, ...style }}
      {...props}
    >
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="-rotate-90" aria-hidden>
        <circle
          cx={size / 2}
          cy={size / 2}
          r={radius}
          fill="none"
          strokeWidth={thickness}
          className="stroke-muted"
        />
        <circle
          cx={size / 2}
          cy={size / 2}
          r={radius}
          fill="none"
          strokeWidth={thickness}
          strokeLinecap="round"
          strokeDasharray={circumference}
          strokeDashoffset={offset}
          className={cn("stroke-current transition-[stroke-dashoffset] duration-700 ease-out", color)}
        />
      </svg>
      {(children || showValue) && (
        <span
          className="absolute inset-0 flex items-center justify-center font-semibold tabular-nums"
          style={{ fontSize: Math.max(size * 0.22, 10) }}
        >
          {children ?? `${Math.round(clamped)}%`}
        </span>
      )}
    </div>
  )
}
```
